#!/usr/bin/env python3
"""
LangGraph Mastery - Complete Book PDF Generator
================================================
Generates a professional 100+ page PDF book covering LangGraph
from fundamentals to advanced production patterns.
"""

from reportlab.lib.pagesizes import letter
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.colors import HexColor, black, white, gray
from reportlab.lib.units import inch, mm
from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_JUSTIFY, TA_RIGHT
from reportlab.platypus import (
    SimpleDocTemplate, Paragraph, Spacer, PageBreak, Table, TableStyle,
    KeepTogether, HRFlowable, ListFlowable, ListItem, Preformatted,
    NextPageTemplate, Frame, PageTemplate, BaseDocTemplate, Image
)
from reportlab.pdfgen import canvas
from reportlab.lib.colors import Color
import textwrap
import os

# ============================================================
# COLOR SCHEME
# ============================================================
COLORS = {
    'primary': HexColor('#1a1a2e'),      # Dark navy
    'secondary': HexColor('#16213e'),     # Deep blue
    'accent': HexColor('#0f3460'),        # Blue
    'highlight': HexColor('#e94560'),     # Red accent
    'code_bg': HexColor('#f4f4f8'),       # Light gray
    'code_border': HexColor('#d1d5db'),   # Gray border
    'success': HexColor('#10b981'),       # Green
    'warning': HexColor('#f59e0b'),       # Amber
    'info': HexColor('#3b82f6'),          # Blue
    'text': HexColor('#1f2937'),          # Dark gray
    'light_text': HexColor('#6b7280'),    # Medium gray
    'chapter_bg': HexColor('#0f3460'),    # Chapter title bg
    'tip_bg': HexColor('#ecfdf5'),        # Light green
    'warning_bg': HexColor('#fffbeb'),    # Light amber
    'note_bg': HexColor('#eff6ff'),       # Light blue
}


# ============================================================
# CUSTOM STYLES
# ============================================================
def get_styles():
    styles = getSampleStyleSheet()
    
    styles.add(ParagraphStyle(
        name='BookTitle',
        fontName='Helvetica-Bold',
        fontSize=36,
        leading=42,
        alignment=TA_CENTER,
        textColor=white,
        spaceAfter=20,
    ))
    
    styles.add(ParagraphStyle(
        name='BookSubtitle',
        fontName='Helvetica',
        fontSize=16,
        leading=22,
        alignment=TA_CENTER,
        textColor=HexColor('#94a3b8'),
        spaceAfter=30,
    ))
    
    styles.add(ParagraphStyle(
        name='ChapterTitle',
        fontName='Helvetica-Bold',
        fontSize=28,
        leading=34,
        textColor=COLORS['primary'],
        spaceAfter=20,
        spaceBefore=10,
    ))
    
    styles.add(ParagraphStyle(
        name='ChapterSubtitle',
        fontName='Helvetica',
        fontSize=13,
        leading=18,
        textColor=COLORS['light_text'],
        spaceAfter=25,
    ))
    
    styles.add(ParagraphStyle(
        name='SectionTitle',
        fontName='Helvetica-Bold',
        fontSize=18,
        leading=24,
        textColor=COLORS['accent'],
        spaceBefore=20,
        spaceAfter=10,
    ))
    
    styles.add(ParagraphStyle(
        name='SubSection',
        fontName='Helvetica-Bold',
        fontSize=14,
        leading=18,
        textColor=COLORS['text'],
        spaceBefore=15,
        spaceAfter=8,
    ))
    
    styles.add(ParagraphStyle(
        name='Body',
        fontName='Helvetica',
        fontSize=10.5,
        leading=16,
        textColor=COLORS['text'],
        alignment=TA_JUSTIFY,
        spaceAfter=8,
        spaceBefore=2,
    ))
    
    styles.add(ParagraphStyle(
        name='CodeBlock',
        fontName='Courier',
        fontSize=8.5,
        leading=12,
        textColor=HexColor('#1e293b'),
        backColor=COLORS['code_bg'],
        leftIndent=15,
        rightIndent=15,
        spaceBefore=8,
        spaceAfter=8,
        borderWidth=0.5,
        borderColor=COLORS['code_border'],
        borderPadding=8,
    ))
    
    styles.add(ParagraphStyle(
        name='InlineCode',
        fontName='Courier',
        fontSize=9.5,
        textColor=COLORS['highlight'],
    ))
    
    styles.add(ParagraphStyle(
        name='TipBox',
        fontName='Helvetica',
        fontSize=10,
        leading=14,
        textColor=HexColor('#065f46'),
        backColor=COLORS['tip_bg'],
        leftIndent=15,
        rightIndent=15,
        spaceBefore=10,
        spaceAfter=10,
        borderWidth=0.5,
        borderColor=COLORS['success'],
        borderPadding=10,
    ))
    
    styles.add(ParagraphStyle(
        name='WarningBox',
        fontName='Helvetica',
        fontSize=10,
        leading=14,
        textColor=HexColor('#92400e'),
        backColor=COLORS['warning_bg'],
        leftIndent=15,
        rightIndent=15,
        spaceBefore=10,
        spaceAfter=10,
        borderWidth=0.5,
        borderColor=COLORS['warning'],
        borderPadding=10,
    ))
    
    styles.add(ParagraphStyle(
        name='NoteBox',
        fontName='Helvetica',
        fontSize=10,
        leading=14,
        textColor=HexColor('#1e40af'),
        backColor=COLORS['note_bg'],
        leftIndent=15,
        rightIndent=15,
        spaceBefore=10,
        spaceAfter=10,
        borderWidth=0.5,
        borderColor=COLORS['info'],
        borderPadding=10,
    ))
    
    styles.add(ParagraphStyle(
        name='KeyTerm',
        fontName='Helvetica-Bold',
        fontSize=11,
        leading=16,
        textColor=COLORS['highlight'],
        spaceBefore=4,
        spaceAfter=4,
    ))
    
    styles.add(ParagraphStyle(
        name='TableHeader',
        fontName='Helvetica-Bold',
        fontSize=9.5,
        leading=13,
        textColor=white,
        alignment=TA_CENTER,
    ))
    
    styles.add(ParagraphStyle(
        name='TableCell',
        fontName='Helvetica',
        fontSize=9,
        leading=12,
        textColor=COLORS['text'],
    ))
    
    styles.add(ParagraphStyle(
        name='TOCEntry',
        fontName='Helvetica',
        fontSize=12,
        leading=20,
        textColor=COLORS['text'],
        leftIndent=20,
    ))
    
    styles.add(ParagraphStyle(
        name='TOCChapter',
        fontName='Helvetica-Bold',
        fontSize=13,
        leading=22,
        textColor=COLORS['primary'],
        spaceBefore=8,
    ))
    
    styles.add(ParagraphStyle(
        name='Footer',
        fontName='Helvetica',
        fontSize=8,
        textColor=COLORS['light_text'],
        alignment=TA_CENTER,
    ))
    
    return styles


# ============================================================
# HELPER FUNCTIONS
# ============================================================

def code(text, styles):
    """Create a code block."""
    clean = text.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
    return Paragraph(f'<pre>{clean}</pre>', styles['CodeBlock'])


def tip(text, styles):
    """Create a tip box."""
    return Paragraph(f'<b>TIP:</b> {text}', styles['TipBox'])


def warning(text, styles):
    """Create a warning box."""
    return Paragraph(f'<b>WARNING:</b> {text}', styles['WarningBox'])


def note(text, styles):
    """Create a note/info box."""
    return Paragraph(f'<b>NOTE:</b> {text}', styles['NoteBox'])


def key_term(term, definition, styles):
    """Create a key term definition."""
    return Paragraph(
        f'<b><font color="#e94560">{term}</font></b> - {definition}',
        styles['Body']
    )


def make_table(headers, rows, styles, col_widths=None):
    """Create a styled table."""
    header_paras = [Paragraph(h, styles['TableHeader']) for h in headers]
    data = [header_paras]
    for row in rows:
        data.append([Paragraph(str(c), styles['TableCell']) for c in row])
    
    if not col_widths:
        col_widths = [6.5 * inch / len(headers)] * len(headers)
    
    t = Table(data, colWidths=col_widths)
    t.setStyle(TableStyle([
        ('BACKGROUND', (0, 0), (-1, 0), COLORS['accent']),
        ('TEXTCOLOR', (0, 0), (-1, 0), white),
        ('ALIGN', (0, 0), (-1, -1), 'LEFT'),
        ('FONTSIZE', (0, 0), (-1, -1), 9),
        ('BOTTOMPADDING', (0, 0), (-1, -1), 8),
        ('TOPPADDING', (0, 0), (-1, -1), 8),
        ('LEFTPADDING', (0, 0), (-1, -1), 8),
        ('GRID', (0, 0), (-1, -1), 0.5, COLORS['code_border']),
        ('ROWBACKGROUNDS', (0, 1), (-1, -1), [white, COLORS['code_bg']]),
        ('VALIGN', (0, 0), (-1, -1), 'TOP'),
    ]))
    return t


def chapter_break(number, title, subtitle, styles):
    """Create a chapter title page."""
    elements = []
    elements.append(PageBreak())
    elements.append(Spacer(1, 2 * inch))
    elements.append(Paragraph(f'CHAPTER {number}', ParagraphStyle(
        'ChapterNum',
        fontName='Helvetica',
        fontSize=14,
        textColor=COLORS['highlight'],
        spaceAfter=10,
        alignment=TA_LEFT,
        letterSpacing=3,
    )))
    elements.append(HRFlowable(
        width="100%", thickness=2, color=COLORS['highlight'],
        spaceAfter=15, spaceBefore=5
    ))
    elements.append(Paragraph(title, styles['ChapterTitle']))
    elements.append(Paragraph(subtitle, styles['ChapterSubtitle']))
    elements.append(HRFlowable(
        width="30%", thickness=1, color=COLORS['accent'],
        spaceAfter=20, spaceBefore=10
    ))
    return elements


def section(title, styles):
    """Create a section header."""
    return Paragraph(title, styles['SectionTitle'])


def subsection(title, styles):
    """Create a subsection header."""
    return Paragraph(title, styles['SubSection'])


def body(text, styles):
    """Create body text."""
    return Paragraph(text, styles['Body'])


def spacer(height=12):
    return Spacer(1, height)


# ============================================================
# PAGE TEMPLATES
# ============================================================

class BookTemplate(BaseDocTemplate):
    def __init__(self, filename, **kwargs):
        super().__init__(filename, **kwargs)
        
        page_w, page_h = letter
        margin = 0.85 * inch
        
        frame = Frame(
            margin, margin + 0.3*inch,
            page_w - 2*margin, page_h - 2*margin - 0.3*inch,
            id='normal'
        )
        
        self.addPageTemplates([
            PageTemplate(id='cover', frames=[Frame(0, 0, page_w, page_h, id='cover_frame')],
                        onPage=self._cover_page),
            PageTemplate(id='content', frames=[frame], onPage=self._content_page),
        ])
    
    def _cover_page(self, canvas_obj, doc):
        pass  # Cover handled by flowables
    
    def _content_page(self, canvas_obj, doc):
        page_w, page_h = letter
        canvas_obj.saveState()
        
        # Header line
        canvas_obj.setStrokeColor(COLORS['accent'])
        canvas_obj.setLineWidth(0.5)
        canvas_obj.line(0.85*inch, page_h - 0.65*inch, page_w - 0.85*inch, page_h - 0.65*inch)
        
        # Header text
        canvas_obj.setFont('Helvetica', 8)
        canvas_obj.setFillColor(COLORS['light_text'])
        canvas_obj.drawString(0.85*inch, page_h - 0.55*inch, "LangGraph Mastery")
        canvas_obj.drawRightString(page_w - 0.85*inch, page_h - 0.55*inch, "From Foundations to Production")
        
        # Footer
        canvas_obj.setFont('Helvetica', 9)
        canvas_obj.setFillColor(COLORS['light_text'])
        canvas_obj.drawCentredString(page_w/2, 0.5*inch, str(doc.page))
        
        canvas_obj.restoreState()


# ============================================================
# BOOK CONTENT
# ============================================================

def build_book():
    output_path = "/home/claude/langgraph-mastery/LangGraph_Mastery_Complete_Book.pdf"
    
    doc = BookTemplate(
        output_path,
        pagesize=letter,
        title="LangGraph Mastery: From Foundations to Production",
        author="AI Architecture Guide",
        subject="Complete LangGraph Tutorial",
    )
    
    styles = get_styles()
    story = []
    
    # ========================================================
    # COVER PAGE
    # ========================================================
    story.append(NextPageTemplate('cover'))
    story.append(Spacer(1, 0.5*inch))
    
    # Cover background table
    cover_data = [['']]
    cover_table = Table(cover_data, colWidths=[7.5*inch], rowHeights=[10*inch])
    cover_table.setStyle(TableStyle([
        ('BACKGROUND', (0,0), (-1,-1), COLORS['primary']),
        ('ALIGN', (0,0), (-1,-1), 'CENTER'),
        ('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
    ]))
    
    # Instead of a full background, use text elements
    story.append(Spacer(1, 1.5*inch))
    
    # Accent line
    story.append(HRFlowable(width="40%", thickness=3, color=COLORS['highlight'],
                            spaceAfter=25, spaceBefore=0))
    
    story.append(Paragraph("LANGGRAPH", ParagraphStyle(
        'CoverMain', fontName='Helvetica-Bold', fontSize=48,
        leading=52, alignment=TA_CENTER, textColor=COLORS['primary'],
    )))
    story.append(Paragraph("MASTERY", ParagraphStyle(
        'CoverMain2', fontName='Helvetica-Bold', fontSize=48,
        leading=52, alignment=TA_CENTER, textColor=COLORS['highlight'],
    )))
    
    story.append(Spacer(1, 15))
    story.append(HRFlowable(width="60%", thickness=1, color=COLORS['accent'],
                            spaceAfter=20))
    
    story.append(Paragraph(
        "From Foundations to Production",
        ParagraphStyle('CoverSub', fontName='Helvetica', fontSize=18,
                      leading=24, alignment=TA_CENTER, textColor=COLORS['accent'])
    ))
    
    story.append(Spacer(1, 10))
    story.append(Paragraph(
        "A Complete Practical Guide to Building AI Agents,<br/>"
        "Multi-Agent Orchestration, and Production Systems",
        ParagraphStyle('CoverDesc', fontName='Helvetica', fontSize=12,
                      leading=18, alignment=TA_CENTER, textColor=COLORS['light_text'])
    ))
    
    story.append(Spacer(1, 1.5*inch))
    
    # Cover details
    story.append(Paragraph(
        "20 Chapters | 22 Python Programs | Real-World Projects",
        ParagraphStyle('CoverDetails', fontName='Helvetica-Bold', fontSize=11,
                      alignment=TA_CENTER, textColor=COLORS['accent'])
    ))
    story.append(Spacer(1, 8))
    story.append(Paragraph(
        "LangGraph | LangChain | LangSmith | MCP | Vector Databases",
        ParagraphStyle('CoverTech', fontName='Courier', fontSize=10,
                      alignment=TA_CENTER, textColor=COLORS['light_text'])
    ))
    
    story.append(Spacer(1, 1*inch))
    story.append(Paragraph(
        "Bootcamp &amp; Hackathon Ready | 2025 Edition",
        ParagraphStyle('CoverEdition', fontName='Helvetica', fontSize=10,
                      alignment=TA_CENTER, textColor=COLORS['light_text'])
    ))
    
    # Switch to content pages
    story.append(NextPageTemplate('content'))
    story.append(PageBreak())
    
    # ========================================================
    # TABLE OF CONTENTS
    # ========================================================
    story.append(Spacer(1, 0.5*inch))
    story.append(Paragraph("TABLE OF CONTENTS", ParagraphStyle(
        'TOCTitle', fontName='Helvetica-Bold', fontSize=24,
        textColor=COLORS['primary'], spaceAfter=30,
    )))
    story.append(HRFlowable(width="100%", thickness=2, color=COLORS['highlight'], spaceAfter=20))
    
    toc_items = [
        ("PART I: FOUNDATIONS", None),
        ("Chapter 1", "Introduction to LangGraph and Agentic AI"),
        ("Chapter 2", "Environment Setup and First Run"),
        ("Chapter 3", "Understanding State: TypedDict, Pydantic, and Annotations"),
        ("Chapter 4", "Your First Graph: Hello LangGraph"),
        ("Chapter 5", "Nodes: The Building Blocks"),
        ("Chapter 6", "Edges, Conditional Routing, and Control Flow"),
        ("", ""),
        ("PART II: CORE CAPABILITIES", None),
        ("Chapter 7", "LangChain Integration and LLM Connections"),
        ("Chapter 8", "Tools, Tool Calling, and Function Binding"),
        ("Chapter 9", "Memory, Persistence, and Checkpointers"),
        ("Chapter 10", "Human-in-the-Loop Patterns"),
        ("", ""),
        ("PART III: ADVANCED PATTERNS", None),
        ("Chapter 11", "Subgraphs and Modular Architecture"),
        ("Chapter 12", "Multi-Agent Orchestration"),
        ("Chapter 13", "RAG with LangGraph: Retrieval Agents"),
        ("Chapter 14", "Vector Databases, Embeddings, and Semantic Search"),
        ("", ""),
        ("PART IV: PRODUCTION SYSTEMS", None),
        ("Chapter 15", "Building Production Chatbots"),
        ("Chapter 16", "LangSmith: Monitoring, Debugging, and Evaluation"),
        ("Chapter 17", "MCP and Agent-to-Agent Communication"),
        ("Chapter 18", "Advanced Patterns: Streaming, Map-Reduce, Error Handling"),
        ("Chapter 19", "Production Deployment and Scaling"),
        ("Chapter 20", "Capstone: Full-Stack AI Agent System"),
    ]
    
    for ch, desc in toc_items:
        if not ch and not desc:
            story.append(Spacer(1, 4))
            continue
        if desc is None:
            story.append(Spacer(1, 8))
            story.append(Paragraph(f'<b>{ch}</b>', ParagraphStyle(
                'TOCPart', fontName='Helvetica-Bold', fontSize=11,
                textColor=COLORS['highlight'], spaceBefore=4, spaceAfter=4,
                letterSpacing=2,
            )))
            continue
        story.append(Paragraph(
            f'<b>{ch}</b>&nbsp;&nbsp;&nbsp;{desc}',
            styles['TOCEntry']
        ))
    
    # ========================================================
    # CHAPTER 1: Introduction to LangGraph
    # ========================================================
    story.extend(chapter_break(1, "Introduction to LangGraph\nand Agentic AI",
                               "Understanding the paradigm shift from chains to graphs", styles))
    
    story.append(section("1.1 What is LangGraph?", styles))
    story.append(body(
        "LangGraph is a framework built by LangChain Inc. for creating <b>stateful, multi-actor applications</b> "
        "using Large Language Models (LLMs). Unlike simple chain-based approaches where you pipe data through "
        "a linear sequence of steps, LangGraph lets you model your application as a <b>graph</b> where nodes "
        "represent computational steps and edges define the flow between them.", styles))
    
    story.append(body(
        "Think of it this way: if LangChain gives you a conveyor belt (linear chains), LangGraph gives you "
        "an entire factory floor where workers (nodes) can pass work to any other worker based on conditions, "
        "loop back for corrections, and maintain shared memory of what has been done.", styles))
    
    story.append(key_term("Graph", "A data structure consisting of nodes (vertices) connected by edges. "
                          "In LangGraph, nodes are functions and edges define execution flow.", styles))
    story.append(key_term("State", "The shared data structure that flows through the graph. Every node "
                          "reads from and writes to this state. It is the 'memory' of your application.", styles))
    story.append(key_term("Agent", "An LLM-powered entity that can reason about which actions to take, "
                          "use tools, and make decisions autonomously.", styles))
    
    story.append(spacer())
    story.append(section("1.2 Why Graphs Instead of Chains?", styles))
    
    story.append(make_table(
        ["Feature", "Chains (LangChain)", "Graphs (LangGraph)"],
        [
            ["Execution Flow", "Linear: A -> B -> C", "Any pattern: loops, branches, parallel"],
            ["State Management", "Pass-through only", "Shared, persistent state"],
            ["Error Recovery", "Restart entire chain", "Retry from any node"],
            ["Multi-Agent", "Not native", "First-class support"],
            ["Human-in-Loop", "Difficult", "Built-in interrupt/resume"],
            ["Debugging", "Limited", "Full trace via LangSmith"],
            ["Cycles/Loops", "Not possible", "Native support"],
        ],
        styles,
        col_widths=[1.5*inch, 2.2*inch, 2.8*inch]
    ))
    
    story.append(spacer())
    story.append(section("1.3 Core Concepts at a Glance", styles))
    
    story.append(body(
        "Before we write any code, let us understand the five pillars of LangGraph:", styles))
    
    concepts = [
        ("StateGraph", "The graph itself. You define it with a state schema, add nodes and edges, then compile it into a runnable application."),
        ("State (TypedDict/Pydantic)", "A typed dictionary or Pydantic model that defines what data flows through your graph. Every node receives the full state and returns updates to it."),
        ("Nodes", "Python functions that perform work. Each node takes the current state as input and returns a dictionary of state updates."),
        ("Edges", "Connections between nodes that define execution order. Can be unconditional (always follow) or conditional (choose path based on state)."),
        ("Checkpointer", "A persistence layer that saves state at each step, enabling time-travel debugging, human-in-the-loop, and crash recovery."),
    ]
    
    for i, (term, defn) in enumerate(concepts, 1):
        story.append(Paragraph(
            f'<b><font color="#e94560">{i}. {term}</font></b> - {defn}',
            styles['Body']
        ))
    
    story.append(spacer())
    story.append(section("1.4 The LangGraph Execution Model", styles))
    
    story.append(code(
        '              +------------------+\n'
        '              |   START (Entry)  |\n'
        '              +--------+---------+\n'
        '                       |\n'
        '                       v\n'
        '              +------------------+\n'
        '              |   Node A         |  <-- Reads state, does work,\n'
        '              |   (function)     |      returns state updates\n'
        '              +--------+---------+\n'
        '                       |\n'
        '              (conditional edge)\n'
        '                 /           \\\n'
        '                v             v\n'
        '     +----------+--+   +-----+--------+\n'
        '     |   Node B    |   |   Node C     |\n'
        '     +----------+--+   +-----+--------+\n'
        '                \\             /\n'
        '                 v           v\n'
        '              +------------------+\n'
        '              |   END (Output)   |\n'
        '              +------------------+',
        styles
    ))
    
    story.append(body(
        "When you invoke a compiled graph, LangGraph: (1) initializes the state, (2) starts at the START node, "
        "(3) executes the first node, (4) evaluates edges to determine the next node, (5) repeats until reaching "
        "END. At each step, the state is updated and optionally checkpointed.", styles))
    
    story.append(spacer())
    story.append(section("1.5 When to Use LangGraph", styles))
    
    story.append(tip(
        "Use LangGraph when your AI application needs: cycles (retry logic, iterative refinement), "
        "branching decisions, shared state across steps, multi-agent coordination, human approval steps, "
        "or crash-resilient execution. For simple prompt-in/response-out tasks, a basic LangChain chain "
        "or even a direct API call is sufficient.", styles))
    
    # ========================================================
    # CHAPTER 2: Environment Setup
    # ========================================================
    story.extend(chapter_break(2, "Environment Setup\nand First Run",
                               "Installing everything you need to build with LangGraph", styles))
    
    story.append(section("2.1 Prerequisites", styles))
    story.append(body("Before starting, ensure you have:", styles))
    
    story.append(make_table(
        ["Requirement", "Version", "Purpose"],
        [
            ["Python", "3.9+", "Runtime environment"],
            ["pip", "Latest", "Package manager"],
            ["OpenAI API Key", "-", "LLM provider (or Anthropic, etc.)"],
            ["LangSmith Account", "Free tier", "Monitoring and debugging"],
            ["VS Code", "Latest", "IDE (recommended)"],
            ["Docker", "Optional", "For vector databases"],
        ],
        styles,
        col_widths=[1.8*inch, 1.2*inch, 3.5*inch]
    ))
    
    story.append(spacer())
    story.append(section("2.2 Project Structure", styles))
    
    story.append(code(
        'langgraph-mastery/\n'
        '|\n'
        '|-- requirements.txt          # All dependencies\n'
        '|-- .env.example              # Environment template\n'
        '|-- README.md                 # Project documentation\n'
        '|\n'
        '|-- src/                      # Numbered source files\n'
        '|   |-- 01_hello_langgraph.py\n'
        '|   |-- 02_state_basics.py\n'
        '|   |-- 03_nodes_and_edges.py\n'
        '|   |-- 04_conditional_routing.py\n'
        '|   |-- 05_llm_integration.py\n'
        '|   |-- 06_tool_calling.py\n'
        '|   |-- 07_memory_persistence.py\n'
        '|   |-- 08_human_in_loop.py\n'
        '|   |-- 09_subgraphs.py\n'
        '|   |-- 10_multi_agent.py\n'
        '|   |-- 11_rag_agent.py\n'
        '|   |-- 12_vector_search.py\n'
        '|   |-- 13_chatbot.py\n'
        '|   |-- 14_langsmith_debug.py\n'
        '|   |-- 15_mcp_agents.py\n'
        '|   |-- 16_streaming.py\n'
        '|   |-- 17_map_reduce.py\n'
        '|   |-- 18_error_handling.py\n'
        '|   |-- 19_deployment.py\n'
        '|   |-- 20_capstone_project.py\n'
        '|   |\n'
        '|   |-- utils/\n'
        '|       |-- __init__.py\n'
        '|       |-- llm_config.py    # LLM setup utilities\n'
        '|       |-- helpers.py       # Common helpers\n'
        '|\n'
        '|-- data/                     # Sample data\n'
        '|-- tests/                    # Unit tests\n'
        '|-- docs/                     # Additional docs\n',
        styles
    ))
    
    story.append(spacer())
    story.append(section("2.3 Installation", styles))
    
    story.append(subsection("Step 1: Create Virtual Environment", styles))
    story.append(code(
        '# Create project directory\n'
        'mkdir langgraph-mastery && cd langgraph-mastery\n'
        '\n'
        '# Create and activate virtual environment\n'
        'python -m venv venv\n'
        'source venv/bin/activate        # macOS/Linux\n'
        'venv\\Scripts\\activate           # Windows',
        styles
    ))
    
    story.append(subsection("Step 2: Install Dependencies", styles))
    story.append(code(
        '# requirements.txt\n'
        'langgraph>=0.2.0\n'
        'langchain>=0.3.0\n'
        'langchain-openai>=0.2.0\n'
        'langchain-anthropic>=0.2.0\n'
        'langchain-community>=0.3.0\n'
        'langsmith>=0.1.0\n'
        'python-dotenv>=1.0.0\n'
        'pydantic>=2.0.0\n'
        'chromadb>=0.5.0\n'
        'faiss-cpu>=1.7.0\n'
        'sentence-transformers>=2.2.0\n'
        'fastapi>=0.110.0\n'
        'uvicorn>=0.27.0\n'
        'httpx>=0.27.0\n'
        '\n'
        '# Install\n'
        'pip install -r requirements.txt',
        styles
    ))
    
    story.append(subsection("Step 3: Configure Environment Variables", styles))
    story.append(code(
        '# .env file\n'
        'OPENAI_API_KEY=sk-your-key-here\n'
        'ANTHROPIC_API_KEY=sk-ant-your-key-here\n'
        '\n'
        '# LangSmith (for monitoring)\n'
        'LANGCHAIN_TRACING_V2=true\n'
        'LANGCHAIN_API_KEY=ls__your-key-here\n'
        'LANGCHAIN_PROJECT=langgraph-mastery',
        styles
    ))
    
    story.append(note(
        "You can get a free LangSmith account at smith.langchain.com. "
        "LangSmith is optional but highly recommended for debugging and monitoring your graphs.", styles))
    
    # ========================================================
    # CHAPTER 3: Understanding State
    # ========================================================
    story.extend(chapter_break(3, "Understanding State",
                               "TypedDict, Pydantic, Annotations, and Reducers - the heart of LangGraph", styles))
    
    story.append(section("3.1 Why State Matters", styles))
    story.append(body(
        "State is the single most important concept in LangGraph. It is the shared data structure that every "
        "node in your graph reads from and writes to. Think of it as a whiteboard in a conference room: every "
        "team member (node) can read what is written and add their own notes. The state carries all the context "
        "your application needs from start to finish.", styles))
    
    story.append(warning(
        "If you do not understand State, you will struggle with everything else in LangGraph. "
        "Take time to master this chapter before moving on.", styles))
    
    story.append(spacer())
    story.append(section("3.2 TypedDict: The Simple Approach", styles))
    
    story.append(key_term("TypedDict",
        "A Python typing construct that defines a dictionary with specific key-value types. "
        "Unlike regular dicts, TypedDict provides type hints so your IDE and type checker know "
        "exactly what keys exist and their types.", styles))
    
    story.append(code(
        '# 02_state_basics.py - Understanding State in LangGraph\n'
        '\n'
        'from typing import TypedDict, Annotated, List\n'
        'from langgraph.graph import StateGraph, START, END\n'
        '\n'
        '# ============================================================\n'
        '# METHOD 1: TypedDict State (Simple, Recommended for Beginners)\n'
        '# ============================================================\n'
        '\n'
        'class SimpleState(TypedDict):\n'
        '    """State for a simple greeting graph.\n'
        '    \n'
        '    Each field is a key in the state dictionary.\n'
        '    Type hints tell LangGraph what type each field should be.\n'
        '    """\n'
        '    user_name: str          # The user\'s name\n'
        '    greeting: str           # The generated greeting\n'
        '    language: str           # Language preference\n'
        '\n'
        '\n'
        '# Node function: receives state, returns state updates\n'
        'def generate_greeting(state: SimpleState) -> dict:\n'
        '    """Generate a greeting based on state.\n'
        '    \n'
        '    KEY CONCEPT: This function receives the FULL state\n'
        '    but only returns the fields it wants to UPDATE.\n'
        '    """\n'
        '    name = state["user_name"]\n'
        '    lang = state.get("language", "English")\n'
        '    \n'
        '    greetings = {\n'
        '        "English": f"Hello, {name}! Welcome to LangGraph!",\n'
        '        "Spanish": f"Hola, {name}! Bienvenido a LangGraph!",\n'
        '        "French": f"Bonjour, {name}! Bienvenue a LangGraph!",\n'
        '    }\n'
        '    \n'
        '    return {"greeting": greetings.get(lang, greetings["English"])}\n'
        '\n'
        '\n'
        '# Build the graph\n'
        'graph = StateGraph(SimpleState)\n'
        'graph.add_node("greeter", generate_greeting)\n'
        'graph.add_edge(START, "greeter")\n'
        'graph.add_edge("greeter", END)\n'
        '\n'
        'app = graph.compile()\n'
        '\n'
        '# Run it\n'
        'result = app.invoke({\n'
        '    "user_name": "Rajesh",\n'
        '    "language": "English"\n'
        '})\n'
        'print(result["greeting"])\n'
        '# Output: Hello, Rajesh! Welcome to LangGraph!',
        styles
    ))
    
    story.append(spacer())
    story.append(section("3.3 Pydantic Models: The Professional Approach", styles))
    
    story.append(key_term("Pydantic",
        "A Python library for data validation using type annotations. Pydantic models automatically "
        "validate data, provide clear error messages, and support complex nested structures. "
        "In LangGraph, Pydantic gives you runtime validation of your state.", styles))
    
    story.append(code(
        'from pydantic import BaseModel, Field\n'
        'from typing import List, Optional\n'
        'from datetime import datetime\n'
        '\n'
        '# ============================================================\n'
        '# METHOD 2: Pydantic State (Production-Grade)\n'
        '# ============================================================\n'
        '\n'
        'class ConversationState(BaseModel):\n'
        '    """Production-grade state with validation.\n'
        '    \n'
        '    ADVANTAGES over TypedDict:\n'
        '    - Runtime validation (catches bad data)\n'
        '    - Default values\n'
        '    - Complex nested types\n'
        '    - Serialization built-in\n'
        '    """\n'
        '    messages: List[dict] = Field(\n'
        '        default_factory=list,\n'
        '        description="Conversation message history"\n'
        '    )\n'
        '    current_agent: str = Field(\n'
        '        default="router",\n'
        '        description="Which agent is currently active"\n'
        '    )\n'
        '    context: Optional[str] = Field(\n'
        '        default=None,\n'
        '        description="Retrieved context for RAG"\n'
        '    )\n'
        '    iteration_count: int = Field(\n'
        '        default=0,\n'
        '        ge=0,            # Must be >= 0\n'
        '        le=10,           # Must be <= 10 (prevent infinite loops)\n'
        '        description="Number of agent iterations"\n'
        '    )\n'
        '    created_at: datetime = Field(\n'
        '        default_factory=datetime.now\n'
        '    )',
        styles
    ))
    
    story.append(spacer())
    story.append(section("3.4 Annotated Reducers: Controlling State Updates", styles))
    
    story.append(key_term("Reducer",
        "A function that defines HOW state updates are merged. Without a reducer, new values "
        "overwrite old values. With a reducer, you can append to lists, increment counters, "
        "or apply any custom merge logic.", styles))
    
    story.append(body(
        "This is one of the most powerful features of LangGraph. By default, when a node returns "
        '<font face="Courier" color="#e94560">{"messages": [new_msg]}</font>, it would replace the entire '
        "messages list. But with the <b>Annotated</b> reducer pattern, you can make it <b>append</b> instead:", styles))
    
    story.append(code(
        'from typing import Annotated\n'
        'from operator import add  # The reducer function\n'
        '\n'
        'class ChatState(TypedDict):\n'
        '    """State with a reducer on messages.\n'
        '    \n'
        '    Annotated[list, add] means:\n'
        '    - The field type is `list`\n'
        '    - When updated, use `add` (which concatenates lists)\n'
        '    - So returning {"messages": [new]} APPENDS to existing list\n'
        '    """\n'
        '    messages: Annotated[list, add]   # <-- APPEND, don\'t replace!\n'
        '    agent_name: str                   # <-- REPLACE (no reducer)\n'
        '\n'
        '\n'
        '# Without reducer: messages = [new_msg]  (REPLACED!)\n'
        '# With reducer:    messages = old_msgs + [new_msg]  (APPENDED!)\n'
        '\n'
        '\n'
        '# Custom reducer example\n'
        'def keep_last_n(existing: list, new: list, n: int = 50) -> list:\n'
        '    """Custom reducer: append but keep only last N items."""\n'
        '    return (existing + new)[-n:]\n'
        '\n'
        'class BoundedState(TypedDict):\n'
        '    messages: Annotated[list, keep_last_n]  # Auto-trim!',
        styles
    ))
    
    story.append(tip(
        "The <b>Annotated[list, add]</b> pattern for messages is so common that LangGraph provides "
        "a built-in helper: <font face='Courier'>from langgraph.graph import MessagesState</font> which "
        "pre-defines a state with an annotated messages list.", styles))
    
    story.append(spacer())
    story.append(section("3.5 MessagesState: The Built-in Shortcut", styles))
    
    story.append(code(
        'from langgraph.graph import MessagesState\n'
        '\n'
        '# MessagesState is equivalent to:\n'
        '# class MessagesState(TypedDict):\n'
        '#     messages: Annotated[list[AnyMessage], add_messages]\n'
        '\n'
        '# You can extend it with your own fields:\n'
        'class MyState(MessagesState):\n'
        '    """Extend built-in MessagesState with custom fields."""\n'
        '    user_id: str\n'
        '    session_id: str\n'
        '    tool_results: Annotated[list, add]\n'
        '    is_complete: bool',
        styles
    ))
    
    # ========================================================
    # CHAPTER 4: Your First Graph
    # ========================================================
    story.extend(chapter_break(4, "Your First Graph:\nHello LangGraph",
                               "Building, compiling, and running your first stateful graph", styles))
    
    story.append(section("4.1 The Complete Hello World", styles))
    
    story.append(body(
        "Let us build a complete graph step by step. This example processes a customer support "
        "request through classification, response generation, and quality checking.", styles))
    
    story.append(code(
        '# 01_hello_langgraph.py\n'
        '"""\n'
        'Your First LangGraph Application\n'
        '=================================\n'
        'A customer support classifier that:\n'
        '1. Classifies the request type\n'
        '2. Generates an appropriate response\n'
        '3. Checks response quality\n'
        '\n'
        'Run: python src/01_hello_langgraph.py\n'
        '"""\n'
        '\n'
        'from typing import TypedDict, Literal\n'
        'from langgraph.graph import StateGraph, START, END\n'
        '\n'
        '\n'
        '# STEP 1: Define State\n'
        '# ====================\n'
        'class SupportState(TypedDict):\n'
        '    """State flowing through our support graph.\n'
        '    \n'
        '    Think of this as a form that gets filled in\n'
        '    as it passes through each department.\n'
        '    """\n'
        '    customer_message: str      # Input from customer\n'
        '    category: str              # Classified category\n'
        '    response: str              # Generated response\n'
        '    quality_score: float       # Quality check result\n'
        '    is_approved: bool          # Whether response passed QA\n'
        '\n'
        '\n'
        '# STEP 2: Define Node Functions\n'
        '# ==============================\n'
        'def classify_request(state: SupportState) -> dict:\n'
        '    """Node 1: Classify the customer request.\n'
        '    \n'
        '    In production, this would use an LLM.\n'
        '    Here we use simple keyword matching.\n'
        '    """\n'
        '    message = state["customer_message"].lower()\n'
        '    \n'
        '    if any(w in message for w in ["bill", "charge", "payment"]):\n'
        '        category = "billing"\n'
        '    elif any(w in message for w in ["bug", "error", "broken"]):\n'
        '        category = "technical"\n'
        '    elif any(w in message for w in ["cancel", "refund", "return"]):\n'
        '        category = "cancellation"\n'
        '    else:\n'
        '        category = "general"\n'
        '    \n'
        '    print(f"  [Classifier] Category: {category}")\n'
        '    return {"category": category}\n'
        '\n'
        '\n'
        'def generate_response(state: SupportState) -> dict:\n'
        '    """Node 2: Generate response based on category."""\n'
        '    responses = {\n'
        '        "billing": "I can help with your billing inquiry...",\n'
        '        "technical": "Let me help troubleshoot that issue...",\n'
        '        "cancellation": "I understand. Let me process that...",\n'
        '        "general": "Thank you for reaching out...",\n'
        '    }\n'
        '    \n'
        '    response = responses.get(state["category"], responses["general"])\n'
        '    print(f"  [Generator] Response ready")\n'
        '    return {"response": response}\n'
        '\n'
        '\n'
        'def check_quality(state: SupportState) -> dict:\n'
        '    """Node 3: Quality check the response."""\n'
        '    score = 0.9 if len(state["response"]) > 20 else 0.3\n'
        '    approved = score > 0.7\n'
        '    print(f"  [QA] Score: {score}, Approved: {approved}")\n'
        '    return {"quality_score": score, "is_approved": approved}\n'
        '\n'
        '\n'
        '# STEP 3: Build the Graph\n'
        '# ========================\n'
        'graph = StateGraph(SupportState)\n'
        '\n'
        '# Add nodes\n'
        'graph.add_node("classify", classify_request)\n'
        'graph.add_node("respond", generate_response)\n'
        'graph.add_node("quality_check", check_quality)\n'
        '\n'
        '# Add edges (define the flow)\n'
        'graph.add_edge(START, "classify")        # Start -> Classify\n'
        'graph.add_edge("classify", "respond")     # Classify -> Respond\n'
        'graph.add_edge("respond", "quality_check") # Respond -> QA\n'
        'graph.add_edge("quality_check", END)      # QA -> End\n'
        '\n'
        '# STEP 4: Compile and Run\n'
        '# ========================\n'
        'app = graph.compile()\n'
        '\n'
        '# Invoke the graph\n'
        'result = app.invoke({\n'
        '    "customer_message": "I found a bug in my dashboard"\n'
        '})\n'
        '\n'
        'print(f"\\nResult:")\n'
        'print(f"  Category: {result[\'category\']}")\n'
        'print(f"  Response: {result[\'response\']}")\n'
        'print(f"  Approved: {result[\'is_approved\']}")',
        styles
    ))
    
    story.append(spacer())
    story.append(section("4.2 Understanding the Execution", styles))
    
    story.append(code(
        'Execution Flow:\n'
        '===============\n'
        '\n'
        '1. app.invoke({"customer_message": "I found a bug..."}) is called\n'
        '\n'
        '2. State initialized:\n'
        '   {\n'
        '     "customer_message": "I found a bug in my dashboard",\n'
        '     "category": "",\n'
        '     "response": "",\n'
        '     "quality_score": 0.0,\n'
        '     "is_approved": False\n'
        '   }\n'
        '\n'
        '3. START -> "classify" node runs:\n'
        '   Returns: {"category": "technical"}\n'
        '   State updated: category = "technical"\n'
        '\n'
        '4. "classify" -> "respond" node runs:\n'
        '   Returns: {"response": "Let me help troubleshoot..."}\n'
        '   State updated: response = "Let me help troubleshoot..."\n'
        '\n'
        '5. "respond" -> "quality_check" node runs:\n'
        '   Returns: {"quality_score": 0.9, "is_approved": True}\n'
        '   State updated: quality_score = 0.9, is_approved = True\n'
        '\n'
        '6. "quality_check" -> END\n'
        '   Final state returned to caller',
        styles
    ))
    
    story.append(note(
        "<b>Key Insight:</b> Each node only returns the fields it changes. LangGraph merges these "
        "updates into the existing state automatically. This is called <b>partial state updates</b>.", styles))
    
    # ========================================================
    # CHAPTER 5: Nodes
    # ========================================================
    story.extend(chapter_break(5, "Nodes:\nThe Building Blocks",
                               "Functions, classes, and patterns for creating powerful graph nodes", styles))
    
    story.append(section("5.1 What is a Node?", styles))
    story.append(body(
        "A node is simply a Python function (or callable) that takes the current state as input "
        "and returns a dictionary of state updates. Nodes are where all the actual work happens: "
        "calling LLMs, querying databases, making API calls, processing data, and making decisions.", styles))
    
    story.append(code(
        '# The simplest possible node\n'
        'def my_node(state: MyState) -> dict:\n'
        '    """Every node follows this pattern:\n'
        '    1. Read from state\n'
        '    2. Do some work\n'
        '    3. Return state updates\n'
        '    """\n'
        '    # 1. Read\n'
        '    data = state["input_data"]\n'
        '    \n'
        '    # 2. Process\n'
        '    result = process(data)\n'
        '    \n'
        '    # 3. Return updates\n'
        '    return {"output_data": result}',
        styles
    ))
    
    story.append(spacer())
    story.append(section("5.2 Node Patterns", styles))
    
    story.append(code(
        '# 03_nodes_and_edges.py - Node Patterns\n'
        '\n'
        'from typing import TypedDict, Annotated\n'
        'from operator import add\n'
        'from langgraph.graph import StateGraph, START, END\n'
        '\n'
        '\n'
        'class ProcessingState(TypedDict):\n'
        '    raw_text: str\n'
        '    steps_completed: Annotated[list, add]  # Track what ran\n'
        '    word_count: int\n'
        '    cleaned_text: str\n'
        '    sentiment: str\n'
        '    summary: str\n'
        '\n'
        '\n'
        '# Pattern 1: Simple Transformation Node\n'
        'def clean_text(state: ProcessingState) -> dict:\n'
        '    """Clean and normalize input text."""\n'
        '    text = state["raw_text"].strip().lower()\n'
        '    text = " ".join(text.split())  # Remove extra whitespace\n'
        '    return {\n'
        '        "cleaned_text": text,\n'
        '        "steps_completed": ["clean_text"]\n'
        '    }\n'
        '\n'
        '\n'
        '# Pattern 2: Analysis Node\n'
        'def analyze_sentiment(state: ProcessingState) -> dict:\n'
        '    """Analyze text sentiment.\n'
        '    In production, this would call an LLM or ML model.\n'
        '    """\n'
        '    text = state["cleaned_text"]\n'
        '    positive = ["good", "great", "excellent", "happy", "love"]\n'
        '    negative = ["bad", "terrible", "awful", "hate", "angry"]\n'
        '    \n'
        '    pos_count = sum(1 for w in positive if w in text)\n'
        '    neg_count = sum(1 for w in negative if w in text)\n'
        '    \n'
        '    if pos_count > neg_count:\n'
        '        sentiment = "positive"\n'
        '    elif neg_count > pos_count:\n'
        '        sentiment = "negative"\n'
        '    else:\n'
        '        sentiment = "neutral"\n'
        '    \n'
        '    return {\n'
        '        "sentiment": sentiment,\n'
        '        "steps_completed": ["analyze_sentiment"]\n'
        '    }\n'
        '\n'
        '\n'
        '# Pattern 3: Counting/Aggregation Node\n'
        'def count_words(state: ProcessingState) -> dict:\n'
        '    """Count words in cleaned text."""\n'
        '    return {\n'
        '        "word_count": len(state["cleaned_text"].split()),\n'
        '        "steps_completed": ["count_words"]\n'
        '    }\n'
        '\n'
        '\n'
        '# Build and run\n'
        'graph = StateGraph(ProcessingState)\n'
        'graph.add_node("clean", clean_text)\n'
        'graph.add_node("sentiment", analyze_sentiment)\n'
        'graph.add_node("count", count_words)\n'
        '\n'
        'graph.add_edge(START, "clean")\n'
        'graph.add_edge("clean", "sentiment")\n'
        'graph.add_edge("clean", "count")      # Parallel execution!\n'
        'graph.add_edge("sentiment", END)\n'
        'graph.add_edge("count", END)\n'
        '\n'
        'app = graph.compile()\n'
        'result = app.invoke({"raw_text": "This product is great!"})\n'
        'print(result)',
        styles
    ))
    
    # ========================================================
    # CHAPTER 6: Conditional Routing
    # ========================================================
    story.extend(chapter_break(6, "Edges, Conditional Routing,\nand Control Flow",
                               "Making your graph smart with dynamic decisions", styles))
    
    story.append(section("6.1 Types of Edges", styles))
    
    story.append(make_table(
        ["Edge Type", "Usage", "Example"],
        [
            ["Normal Edge", "Always goes to next node", "graph.add_edge('A', 'B')"],
            ["Conditional Edge", "Chooses next node based on state", "graph.add_conditional_edges('A', router_fn)"],
            ["Entry Edge", "From START to first node", "graph.add_edge(START, 'A')"],
            ["Exit Edge", "From node to END", "graph.add_edge('A', END)"],
        ],
        styles,
        col_widths=[1.5*inch, 2.2*inch, 2.8*inch]
    ))
    
    story.append(spacer())
    story.append(section("6.2 Conditional Routing", styles))
    
    story.append(body(
        "Conditional edges are what make LangGraph powerful. Instead of a fixed path, "
        "the graph can choose different routes based on the current state. This is how "
        "you implement decision-making, retry logic, and agent reasoning loops.", styles))
    
    story.append(code(
        '# 04_conditional_routing.py\n'
        '\n'
        'from typing import TypedDict, Literal\n'
        'from langgraph.graph import StateGraph, START, END\n'
        '\n'
        '\n'
        'class RouterState(TypedDict):\n'
        '    query: str\n'
        '    query_type: str\n'
        '    response: str\n'
        '    attempts: int\n'
        '\n'
        '\n'
        'def classify_query(state: RouterState) -> dict:\n'
        '    """Classify the incoming query."""\n'
        '    query = state["query"].lower()\n'
        '    if "code" in query or "python" in query:\n'
        '        return {"query_type": "coding"}\n'
        '    elif "math" in query or "calculate" in query:\n'
        '        return {"query_type": "math"}\n'
        '    else:\n'
        '        return {"query_type": "general"}\n'
        '\n'
        '\n'
        'def handle_coding(state: RouterState) -> dict:\n'
        '    return {"response": "Here is a code solution..."}\n'
        '\n'
        '\n'
        'def handle_math(state: RouterState) -> dict:\n'
        '    return {"response": "The calculation result is..."}\n'
        '\n'
        '\n'
        'def handle_general(state: RouterState) -> dict:\n'
        '    return {"response": "Here is a general answer..."}\n'
        '\n'
        '\n'
        '# THE ROUTER FUNCTION\n'
        '# ===================\n'
        '# This function determines which node to go to next.\n'
        '# It reads the state and returns the NAME of the next node.\n'
        '\n'
        'def route_query(state: RouterState) -> Literal[\n'
        '    "coding_handler", "math_handler", "general_handler"\n'
        ']:\n'
        '    """Route to appropriate handler based on query type.\n'
        '    \n'
        '    IMPORTANT: Return value must match a node name!\n'
        '    The Literal type hint helps catch errors.\n'
        '    """\n'
        '    query_type = state["query_type"]\n'
        '    if query_type == "coding":\n'
        '        return "coding_handler"\n'
        '    elif query_type == "math":\n'
        '        return "math_handler"\n'
        '    else:\n'
        '        return "general_handler"\n'
        '\n'
        '\n'
        '# Build graph with conditional routing\n'
        'graph = StateGraph(RouterState)\n'
        '\n'
        'graph.add_node("classify", classify_query)\n'
        'graph.add_node("coding_handler", handle_coding)\n'
        'graph.add_node("math_handler", handle_math)\n'
        'graph.add_node("general_handler", handle_general)\n'
        '\n'
        'graph.add_edge(START, "classify")\n'
        '\n'
        '# Conditional edge: after classify, run route_query\n'
        '# to determine which handler node to execute\n'
        'graph.add_conditional_edges(\n'
        '    "classify",         # Source node\n'
        '    route_query,        # Router function\n'
        '    {                   # Mapping: return value -> node name\n'
        '        "coding_handler": "coding_handler",\n'
        '        "math_handler": "math_handler",\n'
        '        "general_handler": "general_handler",\n'
        '    }\n'
        ')\n'
        '\n'
        '# All handlers lead to END\n'
        'graph.add_edge("coding_handler", END)\n'
        'graph.add_edge("math_handler", END)\n'
        'graph.add_edge("general_handler", END)\n'
        '\n'
        'app = graph.compile()\n'
        '\n'
        '# Test different routes\n'
        'r1 = app.invoke({"query": "Write Python code for sorting"})\n'
        'print(f"Coding: {r1[\'response\']}")\n'
        '\n'
        'r2 = app.invoke({"query": "Calculate 15% of 200"})\n'
        'print(f"Math: {r2[\'response\']}")',
        styles
    ))
    
    story.append(spacer())
    story.append(section("6.3 Cycles and Loops: The Agent Pattern", styles))
    
    story.append(body(
        "One of the most powerful features of LangGraph is its support for cycles. This enables "
        "the classic <b>agent loop pattern</b>: the LLM decides whether to take an action or finish, "
        "and if it takes an action, the result is fed back for another decision.", styles))
    
    story.append(code(
        '# The Agent Loop Pattern\n'
        '#\n'
        '#    START --> agent --> should_continue? --yes--> tool --> agent\n'
        '#                           |                               ^\n'
        '#                           no                              |\n'
        '#                           |                               |\n'
        '#                          END               (loop back)---+\n'
        '\n'
        'def should_continue(state) -> Literal["tool", "__end__"]:\n'
        '    """Check if the agent wants to use a tool or finish."""\n'
        '    last_msg = state["messages"][-1]\n'
        '    if hasattr(last_msg, "tool_calls") and last_msg.tool_calls:\n'
        '        return "tool"       # Agent wants to use a tool\n'
        '    return "__end__"        # Agent is done, go to END',
        styles
    ))
    
    story.append(warning(
        "Always include a maximum iteration check in your loops to prevent infinite execution. "
        "LangGraph has built-in recursion limits (default 25), but you should also implement "
        "your own safeguards in the state.", styles))
    
    # ========================================================
    # CHAPTER 7: LangChain + LLM Integration
    # ========================================================
    story.extend(chapter_break(7, "LangChain Integration\nand LLM Connections",
                               "Connecting your graph to real language models", styles))
    
    story.append(section("7.1 Setting Up LLM Providers", styles))
    
    story.append(code(
        '# 05_llm_integration.py\n'
        '\n'
        'import os\n'
        'from dotenv import load_dotenv\n'
        'from langchain_openai import ChatOpenAI\n'
        'from langchain_anthropic import ChatAnthropic\n'
        'from langchain_core.messages import HumanMessage, SystemMessage\n'
        'from langgraph.graph import StateGraph, MessagesState, START, END\n'
        '\n'
        'load_dotenv()\n'
        '\n'
        '# ============================================================\n'
        '# LLM PROVIDER SETUP\n'
        '# ============================================================\n'
        '\n'
        '# OpenAI\n'
        'openai_llm = ChatOpenAI(\n'
        '    model="gpt-4o-mini",\n'
        '    temperature=0.7,\n'
        '    max_tokens=1000,\n'
        ')\n'
        '\n'
        '# Anthropic\n'
        'anthropic_llm = ChatAnthropic(\n'
        '    model="claude-sonnet-4-20250514",\n'
        '    temperature=0.7,\n'
        '    max_tokens=1000,\n'
        ')\n'
        '\n'
        '\n'
        '# ============================================================\n'
        '# LLM-POWERED NODE\n'
        '# ============================================================\n'
        '\n'
        'def chatbot_node(state: MessagesState) -> dict:\n'
        '    """Call LLM with conversation history.\n'
        '    \n'
        '    MessagesState provides a `messages` field that\n'
        '    automatically accumulates conversation history.\n'
        '    """\n'
        '    response = openai_llm.invoke(state["messages"])\n'
        '    return {"messages": [response]}\n'
        '\n'
        '\n'
        '# Build a simple chatbot graph\n'
        'graph = StateGraph(MessagesState)\n'
        'graph.add_node("chatbot", chatbot_node)\n'
        'graph.add_edge(START, "chatbot")\n'
        'graph.add_edge("chatbot", END)\n'
        '\n'
        'app = graph.compile()\n'
        '\n'
        '# Have a conversation\n'
        'result = app.invoke({\n'
        '    "messages": [\n'
        '        SystemMessage(content="You are a helpful assistant."),\n'
        '        HumanMessage(content="What is LangGraph?")\n'
        '    ]\n'
        '})\n'
        '\n'
        'print(result["messages"][-1].content)',
        styles
    ))
    
    # ========================================================
    # CHAPTER 8: Tools
    # ========================================================
    story.extend(chapter_break(8, "Tools, Tool Calling,\nand Function Binding",
                               "Giving your agents the ability to interact with the world", styles))
    
    story.append(section("8.1 What are Tools?", styles))
    story.append(body(
        "Tools are functions that an LLM can decide to call. When an LLM has tools bound to it, "
        "it can examine the user's request and decide whether to call one or more tools to get "
        "information or take action. This is the foundation of agentic AI: the LLM reasons about "
        "which tools to use and interprets their results.", styles))
    
    story.append(code(
        '# 06_tool_calling.py\n'
        '\n'
        'from langchain_core.tools import tool\n'
        'from langchain_openai import ChatOpenAI\n'
        'from langgraph.graph import StateGraph, MessagesState, START, END\n'
        'from langgraph.prebuilt import ToolNode, tools_condition\n'
        '\n'
        '\n'
        '# ============================================================\n'
        '# DEFINE TOOLS\n'
        '# ============================================================\n'
        '\n'
        '@tool\n'
        'def search_web(query: str) -> str:\n'
        '    """Search the web for information.\n'
        '    \n'
        '    Args:\n'
        '        query: The search query string\n'
        '    \n'
        '    Returns:\n'
        '        Search results as a string\n'
        '    """\n'
        '    # In production, call a real search API\n'
        '    return f"Search results for: {query}"\n'
        '\n'
        '\n'
        '@tool\n'
        'def calculate(expression: str) -> str:\n'
        '    """Evaluate a mathematical expression.\n'
        '    \n'
        '    Args:\n'
        '        expression: Math expression like \'2 + 2\'\n'
        '    """\n'
        '    try:\n'
        '        result = eval(expression)  # Use safer evaluation in prod\n'
        '        return f"Result: {result}"\n'
        '    except Exception as e:\n'
        '        return f"Error: {str(e)}"\n'
        '\n'
        '\n'
        '@tool\n'
        'def get_weather(city: str) -> str:\n'
        '    """Get current weather for a city.\n'
        '    \n'
        '    Args:\n'
        '        city: Name of the city\n'
        '    """\n'
        '    return f"Weather in {city}: 72F, Sunny"\n'
        '\n'
        '\n'
        '# ============================================================\n'
        '# BIND TOOLS TO LLM\n'
        '# ============================================================\n'
        '\n'
        'tools = [search_web, calculate, get_weather]\n'
        '\n'
        '# bind_tools tells the LLM about available tools\n'
        'llm = ChatOpenAI(model="gpt-4o-mini").bind_tools(tools)\n'
        '\n'
        '\n'
        'def agent_node(state: MessagesState) -> dict:\n'
        '    """The agent: decides whether to use tools or respond."""\n'
        '    response = llm.invoke(state["messages"])\n'
        '    return {"messages": [response]}\n'
        '\n'
        '\n'
        '# ============================================================\n'
        '# BUILD THE AGENT GRAPH\n'
        '# ============================================================\n'
        '\n'
        'graph = StateGraph(MessagesState)\n'
        '\n'
        '# The agent node (LLM that can call tools)\n'
        'graph.add_node("agent", agent_node)\n'
        '\n'
        '# The tool node (executes tool calls)\n'
        'graph.add_node("tools", ToolNode(tools))\n'
        '\n'
        '# Flow: START -> agent\n'
        'graph.add_edge(START, "agent")\n'
        '\n'
        '# Conditional: after agent, check if tools needed\n'
        '# tools_condition is a built-in that checks for tool_calls\n'
        'graph.add_conditional_edges(\n'
        '    "agent",\n'
        '    tools_condition,  # Returns "tools" or "__end__"\n'
        ')\n'
        '\n'
        '# After tools execute, go back to agent (THE LOOP!)\n'
        'graph.add_edge("tools", "agent")\n'
        '\n'
        'app = graph.compile()\n'
        '\n'
        '# The agent will:\n'
        '# 1. See the query\n'
        '# 2. Decide to call get_weather tool\n'
        '# 3. Get the result\n'
        '# 4. Formulate a response\n'
        'result = app.invoke({\n'
        '    "messages": [("user", "What is the weather in Paris?")]\n'
        '})\n'
        'print(result["messages"][-1].content)',
        styles
    ))
    
    story.append(key_term("ToolNode",
        "A prebuilt LangGraph node that automatically executes tool calls from the LLM response. "
        "It reads tool_calls from the message, executes each tool, and returns ToolMessage results.", styles))
    
    story.append(key_term("tools_condition",
        "A prebuilt routing function that checks if the LLM's response contains tool calls. "
        "Returns 'tools' if tools need to be executed, or '__end__' if the LLM is done.", styles))
    
    # ========================================================
    # CHAPTER 9: Memory and Persistence
    # ========================================================
    story.extend(chapter_break(9, "Memory, Persistence,\nand Checkpointers",
                               "Making your agents remember across conversations", styles))
    
    story.append(section("9.1 The Checkpointer Pattern", styles))
    
    story.append(body(
        "In production, you need your graph to remember state between invocations. "
        "LangGraph's checkpointer system saves the state at each step, enabling: "
        "conversation memory across sessions, crash recovery from the last good state, "
        "time-travel debugging (inspect any past state), and human-in-the-loop (pause and resume).", styles))
    
    story.append(code(
        '# 07_memory_persistence.py\n'
        '\n'
        'from langgraph.graph import StateGraph, MessagesState, START, END\n'
        'from langgraph.checkpoint.memory import MemorySaver\n'
        'from langchain_core.messages import HumanMessage\n'
        '\n'
        '\n'
        '# ============================================================\n'
        '# CHECKPOINTER: In-Memory (for development)\n'
        '# ============================================================\n'
        '\n'
        'checkpointer = MemorySaver()\n'
        '\n'
        '\n'
        'def echo_bot(state: MessagesState) -> dict:\n'
        '    """Simple bot that echoes and counts messages."""\n'
        '    count = len(state["messages"])\n'
        '    last = state["messages"][-1].content\n'
        '    reply = f"[Message #{count}] You said: {last}"\n'
        '    return {"messages": [{"role": "assistant", "content": reply}]}\n'
        '\n'
        '\n'
        'graph = StateGraph(MessagesState)\n'
        'graph.add_node("bot", echo_bot)\n'
        'graph.add_edge(START, "bot")\n'
        'graph.add_edge("bot", END)\n'
        '\n'
        '# Compile WITH checkpointer\n'
        'app = graph.compile(checkpointer=checkpointer)\n'
        '\n'
        '\n'
        '# ============================================================\n'
        '# THREAD-BASED CONVERSATIONS\n'
        '# ============================================================\n'
        '\n'
        '# Thread 1: Conversation with Alice\n'
        'config_alice = {"configurable": {"thread_id": "alice_123"}}\n'
        '\n'
        'app.invoke(\n'
        '    {"messages": [HumanMessage(content="Hello!")]},\n'
        '    config=config_alice\n'
        ')\n'
        'app.invoke(\n'
        '    {"messages": [HumanMessage(content="How are you?")]},\n'
        '    config=config_alice\n'
        ')\n'
        '\n'
        '# Thread 2: Separate conversation with Bob\n'
        'config_bob = {"configurable": {"thread_id": "bob_456"}}\n'
        '\n'
        'app.invoke(\n'
        '    {"messages": [HumanMessage(content="Hi there!")]},\n'
        '    config=config_bob\n'
        ')\n'
        '\n'
        '# Alice\'s conversation has 4 messages (2 human + 2 bot)\n'
        '# Bob\'s conversation has 2 messages (1 human + 1 bot)\n'
        '# They are SEPARATE threads!',
        styles
    ))
    
    story.append(key_term("thread_id",
        "A unique identifier for a conversation thread. Each thread has its own state history. "
        "This is how you maintain separate conversations for different users.", styles))
    
    story.append(key_term("MemorySaver",
        "An in-memory checkpointer for development. For production, use SqliteSaver "
        "(file-based) or PostgresSaver (database-backed) for persistence across restarts.", styles))
    
    # ========================================================
    # CHAPTER 10: Human-in-the-Loop
    # ========================================================
    story.extend(chapter_break(10, "Human-in-the-Loop\nPatterns",
                               "Adding human oversight and approval to your agent workflows", styles))
    
    story.append(section("10.1 The Interrupt Pattern", styles))
    
    story.append(body(
        "Sometimes you want a human to review and approve an agent's action before it executes. "
        "LangGraph supports this through the <b>interrupt</b> mechanism: the graph pauses execution "
        "at a specified node, waits for human input, and then resumes from where it left off.", styles))
    
    story.append(code(
        '# 08_human_in_loop.py\n'
        '\n'
        'from typing import TypedDict\n'
        'from langgraph.graph import StateGraph, START, END\n'
        'from langgraph.checkpoint.memory import MemorySaver\n'
        '\n'
        '\n'
        'class ApprovalState(TypedDict):\n'
        '    request: str\n'
        '    analysis: str\n'
        '    approved: bool\n'
        '    final_action: str\n'
        '\n'
        '\n'
        'def analyze_request(state: ApprovalState) -> dict:\n'
        '    """Analyze the request and prepare recommendation."""\n'
        '    return {\n'
        '        "analysis": f"Analyzed: {state[\'request\']}. "\n'
        '                   f"Recommendation: APPROVE"\n'
        '    }\n'
        '\n'
        '\n'
        'def execute_action(state: ApprovalState) -> dict:\n'
        '    """Execute the approved action."""\n'
        '    if state["approved"]:\n'
        '        return {"final_action": "Action executed successfully!"}\n'
        '    return {"final_action": "Action was rejected by human."}\n'
        '\n'
        '\n'
        'graph = StateGraph(ApprovalState)\n'
        'graph.add_node("analyze", analyze_request)\n'
        'graph.add_node("execute", execute_action)\n'
        '\n'
        'graph.add_edge(START, "analyze")\n'
        'graph.add_edge("analyze", "execute")\n'
        'graph.add_edge("execute", END)\n'
        '\n'
        '# Compile with interrupt BEFORE execute node\n'
        'checkpointer = MemorySaver()\n'
        'app = graph.compile(\n'
        '    checkpointer=checkpointer,\n'
        '    interrupt_before=["execute"]  # Pause here for approval!\n'
        ')\n'
        '\n'
        '# Step 1: Start the graph (will pause before "execute")\n'
        'config = {"configurable": {"thread_id": "approval_1"}}\n'
        'result = app.invoke(\n'
        '    {"request": "Deploy to production"},\n'
        '    config=config\n'
        ')\n'
        'print(f"Analysis: {result[\'analysis\']}")\n'
        'print("Waiting for human approval...")\n'
        '\n'
        '# Step 2: Human reviews and approves\n'
        '# (In a real app, this comes from a UI/API)\n'
        'app.update_state(\n'
        '    config,\n'
        '    {"approved": True}  # Human approves!\n'
        ')\n'
        '\n'
        '# Step 3: Resume execution\n'
        'result = app.invoke(None, config=config)\n'
        'print(f"Result: {result[\'final_action\']}")',
        styles
    ))
    
    # ========================================================
    # CHAPTER 11: Subgraphs
    # ========================================================
    story.extend(chapter_break(11, "Subgraphs and\nModular Architecture",
                               "Breaking complex systems into composable pieces", styles))
    
    story.append(section("11.1 Why Subgraphs?", styles))
    story.append(body(
        "As your graphs grow in complexity, you need to break them into reusable, testable modules. "
        "Subgraphs let you nest one compiled graph inside another, each with its own state schema. "
        "This is like creating functions in programming: each subgraph has a clear interface.", styles))
    
    story.append(code(
        '# 09_subgraphs.py\n'
        '\n'
        'from typing import TypedDict\n'
        'from langgraph.graph import StateGraph, START, END\n'
        '\n'
        '\n'
        '# ============================================================\n'
        '# SUBGRAPH: Research Agent\n'
        '# ============================================================\n'
        '\n'
        'class ResearchState(TypedDict):\n'
        '    topic: str\n'
        '    findings: str\n'
        '\n'
        '\n'
        'def search_topic(state: ResearchState) -> dict:\n'
        '    return {"findings": f"Research on {state[\'topic\']}: Found 5 sources."}\n'
        '\n'
        'research_graph = StateGraph(ResearchState)\n'
        'research_graph.add_node("search", search_topic)\n'
        'research_graph.add_edge(START, "search")\n'
        'research_graph.add_edge("search", END)\n'
        'research_subgraph = research_graph.compile()\n'
        '\n'
        '\n'
        '# ============================================================\n'
        '# SUBGRAPH: Writing Agent\n'
        '# ============================================================\n'
        '\n'
        'class WritingState(TypedDict):\n'
        '    content: str\n'
        '    draft: str\n'
        '\n'
        '\n'
        'def write_draft(state: WritingState) -> dict:\n'
        '    return {"draft": f"Draft based on: {state[\'content\']}"}\n'
        '\n'
        'writing_graph = StateGraph(WritingState)\n'
        'writing_graph.add_node("write", write_draft)\n'
        'writing_graph.add_edge(START, "write")\n'
        'writing_graph.add_edge("write", END)\n'
        'writing_subgraph = writing_graph.compile()\n'
        '\n'
        '\n'
        '# ============================================================\n'
        '# PARENT GRAPH: Orchestrator\n'
        '# ============================================================\n'
        '\n'
        'class OrchestratorState(TypedDict):\n'
        '    topic: str\n'
        '    research: str\n'
        '    article: str\n'
        '\n'
        '\n'
        'def do_research(state: OrchestratorState) -> dict:\n'
        '    """Call research subgraph."""\n'
        '    result = research_subgraph.invoke({"topic": state["topic"]})\n'
        '    return {"research": result["findings"]}\n'
        '\n'
        '\n'
        'def do_writing(state: OrchestratorState) -> dict:\n'
        '    """Call writing subgraph."""\n'
        '    result = writing_subgraph.invoke({"content": state["research"]})\n'
        '    return {"article": result["draft"]}\n'
        '\n'
        '\n'
        'main_graph = StateGraph(OrchestratorState)\n'
        'main_graph.add_node("research", do_research)\n'
        'main_graph.add_node("writing", do_writing)\n'
        'main_graph.add_edge(START, "research")\n'
        'main_graph.add_edge("research", "writing")\n'
        'main_graph.add_edge("writing", END)\n'
        '\n'
        'app = main_graph.compile()\n'
        'result = app.invoke({"topic": "AI Agents in Healthcare"})\n'
        'print(result["article"])',
        styles
    ))
    
    # ========================================================
    # CHAPTER 12: Multi-Agent
    # ========================================================
    story.extend(chapter_break(12, "Multi-Agent\nOrchestration",
                               "Building systems where multiple AI agents collaborate", styles))
    
    story.append(section("12.1 Orchestration Patterns", styles))
    
    story.append(make_table(
        ["Pattern", "Description", "Use Case"],
        [
            ["Supervisor", "One agent delegates to specialists", "Customer support routing"],
            ["Hierarchical", "Multi-level delegation tree", "Complex research tasks"],
            ["Collaborative", "Agents pass work peer-to-peer", "Code review, writing"],
            ["Competitive", "Multiple agents solve, best wins", "Creative generation"],
            ["Swarm", "Dynamic handoff between agents", "Complex conversations"],
        ],
        styles,
        col_widths=[1.5*inch, 2.5*inch, 2.5*inch]
    ))
    
    story.append(spacer())
    story.append(section("12.2 Supervisor Pattern Implementation", styles))
    
    story.append(code(
        '# 10_multi_agent.py - Supervisor Pattern\n'
        '\n'
        'from typing import TypedDict, Annotated, Literal\n'
        'from operator import add\n'
        'from langgraph.graph import StateGraph, MessagesState, START, END\n'
        '\n'
        '\n'
        'class TeamState(TypedDict):\n'
        '    task: str\n'
        '    messages: Annotated[list, add]\n'
        '    next_agent: str\n'
        '    result: str\n'
        '\n'
        '\n'
        '# SPECIALIST AGENTS\n'
        'def researcher(state: TeamState) -> dict:\n'
        '    """Research specialist agent."""\n'
        '    return {\n'
        '        "messages": [f"[Researcher] Found data on: {state[\'task\']}"],\n'
        '        "result": "Research complete"\n'
        '    }\n'
        '\n'
        '\n'
        'def writer(state: TeamState) -> dict:\n'
        '    """Writing specialist agent."""\n'
        '    return {\n'
        '        "messages": [f"[Writer] Drafted content for: {state[\'task\']}"],\n'
        '        "result": "Draft complete"\n'
        '    }\n'
        '\n'
        '\n'
        'def reviewer(state: TeamState) -> dict:\n'
        '    """Review specialist agent."""\n'
        '    return {\n'
        '        "messages": [f"[Reviewer] Reviewed: {state[\'task\']}"],\n'
        '        "result": "Review complete"\n'
        '    }\n'
        '\n'
        '\n'
        '# SUPERVISOR: Routes tasks to the right agent\n'
        'def supervisor(state: TeamState) -> dict:\n'
        '    """Supervisor decides which agent to call next.\n'
        '    In production, this would use an LLM to decide.\n'
        '    """\n'
        '    task = state["task"].lower()\n'
        '    \n'
        '    if "research" in task:\n'
        '        return {"next_agent": "researcher"}\n'
        '    elif "write" in task:\n'
        '        return {"next_agent": "writer"}\n'
        '    elif "review" in task:\n'
        '        return {"next_agent": "reviewer"}\n'
        '    else:\n'
        '        return {"next_agent": "FINISH"}\n'
        '\n'
        '\n'
        'def route_to_agent(state: TeamState) -> str:\n'
        '    """Route based on supervisor\'s decision."""\n'
        '    agent = state["next_agent"]\n'
        '    if agent == "FINISH":\n'
        '        return "__end__"\n'
        '    return agent\n'
        '\n'
        '\n'
        '# Build the multi-agent graph\n'
        'graph = StateGraph(TeamState)\n'
        '\n'
        'graph.add_node("supervisor", supervisor)\n'
        'graph.add_node("researcher", researcher)\n'
        'graph.add_node("writer", writer)\n'
        'graph.add_node("reviewer", reviewer)\n'
        '\n'
        'graph.add_edge(START, "supervisor")\n'
        'graph.add_conditional_edges("supervisor", route_to_agent)\n'
        '\n'
        '# After each agent, go back to supervisor\n'
        'graph.add_edge("researcher", "supervisor")\n'
        'graph.add_edge("writer", "supervisor")\n'
        'graph.add_edge("reviewer", "supervisor")\n'
        '\n'
        'app = graph.compile()\n'
        'result = app.invoke({"task": "research AI trends"})\n'
        'print(result)',
        styles
    ))
    
    # ========================================================
    # CHAPTER 13: RAG with LangGraph
    # ========================================================
    story.extend(chapter_break(13, "RAG with LangGraph:\nRetrieval Agents",
                               "Building intelligent retrieval-augmented generation systems", styles))
    
    story.append(section("13.1 The RAG Agent Pattern", styles))
    
    story.append(body(
        "A RAG agent goes beyond simple retrieve-then-answer. With LangGraph, you can build agents that "
        "decide whether to search, what to search for, evaluate retrieval quality, and iterate until "
        "they have sufficient context to answer accurately.", styles))
    
    story.append(code(
        '# 11_rag_agent.py - RAG with LangGraph\n'
        '\n'
        'from typing import TypedDict, Annotated, List\n'
        'from operator import add\n'
        'from langgraph.graph import StateGraph, START, END\n'
        '\n'
        '\n'
        'class RAGState(TypedDict):\n'
        '    question: str\n'
        '    search_queries: list\n'
        '    retrieved_docs: Annotated[list, add]\n'
        '    answer: str\n'
        '    confidence: float\n'
        '    iteration: int\n'
        '\n'
        '\n'
        'def generate_queries(state: RAGState) -> dict:\n'
        '    """Generate search queries from the question.\n'
        '    In production, use an LLM for query expansion.\n'
        '    """\n'
        '    q = state["question"]\n'
        '    queries = [\n'
        '        q,\n'
        '        f"{q} definition",\n'
        '        f"{q} examples"\n'
        '    ]\n'
        '    return {"search_queries": queries}\n'
        '\n'
        '\n'
        'def retrieve_documents(state: RAGState) -> dict:\n'
        '    """Retrieve documents for each query.\n'
        '    In production, call your vector database.\n'
        '    """\n'
        '    docs = []\n'
        '    for query in state["search_queries"]:\n'
        '        # Simulated retrieval\n'
        '        docs.append({\n'
        '            "query": query,\n'
        '            "content": f"Document about: {query}",\n'
        '            "score": 0.85\n'
        '        })\n'
        '    return {"retrieved_docs": docs}\n'
        '\n'
        '\n'
        'def evaluate_docs(state: RAGState) -> dict:\n'
        '    """Evaluate if retrieved docs are sufficient."""\n'
        '    avg_score = sum(d["score"] for d in state["retrieved_docs"])\n'
        '    avg_score /= max(len(state["retrieved_docs"]), 1)\n'
        '    return {\n'
        '        "confidence": avg_score,\n'
        '        "iteration": state.get("iteration", 0) + 1\n'
        '    }\n'
        '\n'
        '\n'
        'def generate_answer(state: RAGState) -> dict:\n'
        '    """Generate final answer from retrieved context."""\n'
        '    context = "\\n".join([d["content"] for d in state["retrieved_docs"]])\n'
        '    answer = f"Based on {len(state[\'retrieved_docs\'])} sources: {context}"\n'
        '    return {"answer": answer}\n'
        '\n'
        '\n'
        'def should_retry(state: RAGState) -> str:\n'
        '    """Decide: generate answer or retry retrieval?"""\n'
        '    if state["confidence"] >= 0.8 or state["iteration"] >= 3:\n'
        '        return "generate"    # Good enough or max retries\n'
        '    return "retry"           # Need more context\n'
        '\n'
        '\n'
        '# Build RAG graph with retry loop\n'
        'graph = StateGraph(RAGState)\n'
        '\n'
        'graph.add_node("gen_queries", generate_queries)\n'
        'graph.add_node("retrieve", retrieve_documents)\n'
        'graph.add_node("evaluate", evaluate_docs)\n'
        'graph.add_node("generate", generate_answer)\n'
        '\n'
        'graph.add_edge(START, "gen_queries")\n'
        'graph.add_edge("gen_queries", "retrieve")\n'
        'graph.add_edge("retrieve", "evaluate")\n'
        'graph.add_conditional_edges("evaluate", should_retry, {\n'
        '    "generate": "generate",\n'
        '    "retry": "gen_queries"     # Loop back!\n'
        '})\n'
        'graph.add_edge("generate", END)\n'
        '\n'
        'app = graph.compile()\n'
        'result = app.invoke({"question": "What is LangGraph?"})\n'
        'print(f"Answer: {result[\'answer\']}")\n'
        'print(f"Confidence: {result[\'confidence\']}")\n'
        'print(f"Iterations: {result[\'iteration\']}")',
        styles
    ))
    
    # ========================================================
    # CHAPTER 14: Vector Databases
    # ========================================================
    story.extend(chapter_break(14, "Vector Databases,\nEmbeddings, and Search",
                               "The retrieval infrastructure for your AI agents", styles))
    
    story.append(section("14.1 Embedding Models", styles))
    
    story.append(make_table(
        ["Model", "Dimensions", "Speed", "Quality"],
        [
            ["all-MiniLM-L6-v2", "384", "Fast (local)", "Good for general use"],
            ["all-mpnet-base-v2", "768", "Medium (local)", "Better quality"],
            ["text-embedding-3-small", "1536", "API call", "OpenAI, cost-effective"],
            ["text-embedding-3-large", "3072", "API call", "OpenAI, highest quality"],
            ["voyage-3", "1024", "API call", "Excellent for code/technical"],
        ],
        styles,
    ))
    
    story.append(spacer())
    story.append(section("14.2 ChromaDB Integration", styles))
    
    story.append(code(
        '# 12_vector_search.py\n'
        '\n'
        'import chromadb\n'
        'from sentence_transformers import SentenceTransformer\n'
        '\n'
        '\n'
        'class VectorStore:\n'
        '    """Simple vector store using ChromaDB."""\n'
        '    \n'
        '    def __init__(self, collection_name="knowledge_base"):\n'
        '        self.client = chromadb.Client()  # In-memory\n'
        '        self.collection = self.client.create_collection(\n'
        '            name=collection_name,\n'
        '            metadata={"hnsw:space": "cosine"}\n'
        '        )\n'
        '        self.embedder = SentenceTransformer("all-MiniLM-L6-v2")\n'
        '    \n'
        '    def add_documents(self, docs: list, ids: list):\n'
        '        """Add documents to the vector store."""\n'
        '        embeddings = self.embedder.encode(docs).tolist()\n'
        '        self.collection.add(\n'
        '            embeddings=embeddings,\n'
        '            documents=docs,\n'
        '            ids=ids\n'
        '        )\n'
        '    \n'
        '    def search(self, query: str, top_k: int = 3) -> list:\n'
        '        """Search for similar documents."""\n'
        '        query_embedding = self.embedder.encode([query]).tolist()\n'
        '        results = self.collection.query(\n'
        '            query_embeddings=query_embedding,\n'
        '            n_results=top_k\n'
        '        )\n'
        '        return results["documents"][0]\n'
        '\n'
        '\n'
        '# Usage\n'
        'store = VectorStore()\n'
        'store.add_documents(\n'
        '    docs=[\n'
        '        "LangGraph is a framework for building AI agents",\n'
        '        "Vector databases store embeddings for similarity search",\n'
        '        "RAG combines retrieval with generation for accurate answers",\n'
        '    ],\n'
        '    ids=["doc1", "doc2", "doc3"]\n'
        ')\n'
        '\n'
        'results = store.search("How do AI agents work?")\n'
        'for doc in results:\n'
        '    print(f"Found: {doc}")',
        styles
    ))
    
    # ========================================================
    # CHAPTER 15: Chatbot
    # ========================================================
    story.extend(chapter_break(15, "Building Production\nChatbots",
                               "From simple chatbot to full-featured conversational AI", styles))
    
    story.append(section("15.1 Production Chatbot Architecture", styles))
    
    story.append(code(
        '# 13_chatbot.py - Production Chatbot\n'
        '\n'
        'from typing import TypedDict, Annotated\n'
        'from operator import add\n'
        'from langgraph.graph import StateGraph, START, END\n'
        'from langgraph.checkpoint.memory import MemorySaver\n'
        'from langchain_core.messages import HumanMessage, AIMessage, SystemMessage\n'
        '\n'
        '\n'
        'class ChatbotState(TypedDict):\n'
        '    messages: Annotated[list, add]\n'
        '    user_id: str\n'
        '    context: str\n'
        '    intent: str\n'
        '\n'
        '\n'
        'def detect_intent(state: ChatbotState) -> dict:\n'
        '    """Detect user intent from the last message."""\n'
        '    msg = state["messages"][-1]\n'
        '    content = msg.content if hasattr(msg, "content") else str(msg)\n'
        '    content = content.lower()\n'
        '    \n'
        '    if any(w in content for w in ["help", "support", "issue"]):\n'
        '        return {"intent": "support"}\n'
        '    elif any(w in content for w in ["buy", "price", "order"]):\n'
        '        return {"intent": "sales"}\n'
        '    elif any(w in content for w in ["bye", "thanks", "done"]):\n'
        '        return {"intent": "farewell"}\n'
        '    return {"intent": "general"}\n'
        '\n'
        '\n'
        'def route_intent(state: ChatbotState) -> str:\n'
        '    intent = state.get("intent", "general")\n'
        '    return {"support": "support_agent", "sales": "sales_agent",\n'
        '            "farewell": "farewell_agent"}.get(intent, "general_agent")\n'
        '\n'
        '\n'
        'def support_agent(state: ChatbotState) -> dict:\n'
        '    return {"messages": [AIMessage(\n'
        '        content="I am here to help! What issue are you facing?"\n'
        '    )]}\n'
        '\n'
        '\n'
        'def sales_agent(state: ChatbotState) -> dict:\n'
        '    return {"messages": [AIMessage(\n'
        '        content="Great! Let me help you find the right product."\n'
        '    )]}\n'
        '\n'
        '\n'
        'def general_agent(state: ChatbotState) -> dict:\n'
        '    return {"messages": [AIMessage(\n'
        '        content="How can I assist you today?"\n'
        '    )]}\n'
        '\n'
        '\n'
        'def farewell_agent(state: ChatbotState) -> dict:\n'
        '    return {"messages": [AIMessage(\n'
        '        content="Goodbye! Have a great day!"\n'
        '    )]}\n'
        '\n'
        '\n'
        '# Build chatbot graph\n'
        'graph = StateGraph(ChatbotState)\n'
        'graph.add_node("detect", detect_intent)\n'
        'graph.add_node("support_agent", support_agent)\n'
        'graph.add_node("sales_agent", sales_agent)\n'
        'graph.add_node("general_agent", general_agent)\n'
        'graph.add_node("farewell_agent", farewell_agent)\n'
        '\n'
        'graph.add_edge(START, "detect")\n'
        'graph.add_conditional_edges("detect", route_intent)\n'
        'graph.add_edge("support_agent", END)\n'
        'graph.add_edge("sales_agent", END)\n'
        'graph.add_edge("general_agent", END)\n'
        'graph.add_edge("farewell_agent", END)\n'
        '\n'
        'checkpointer = MemorySaver()\n'
        'chatbot = graph.compile(checkpointer=checkpointer)',
        styles
    ))
    
    # ========================================================
    # CHAPTER 16: LangSmith
    # ========================================================
    story.extend(chapter_break(16, "LangSmith: Monitoring,\nDebugging, and Evaluation",
                               "Professional observability for your LangGraph applications", styles))
    
    story.append(section("16.1 Setting Up LangSmith", styles))
    
    story.append(code(
        '# 14_langsmith_debug.py\n'
        '\n'
        'import os\n'
        '\n'
        '# ============================================================\n'
        '# LANGSMITH CONFIGURATION\n'
        '# ============================================================\n'
        '\n'
        '# Set these environment variables:\n'
        'os.environ["LANGCHAIN_TRACING_V2"] = "true"\n'
        'os.environ["LANGCHAIN_API_KEY"] = "ls__your-key"\n'
        'os.environ["LANGCHAIN_PROJECT"] = "langgraph-mastery"\n'
        '\n'
        '# That is it! All LangGraph runs are now traced.\n'
        '# View traces at: https://smith.langchain.com\n'
        '\n'
        '\n'
        '# ============================================================\n'
        '# WHAT LANGSMITH SHOWS YOU\n'
        '# ============================================================\n'
        '\n'
        '# For each graph execution, you can see:\n'
        '#\n'
        '# 1. TRACE VIEW\n'
        '#    - Every node execution in order\n'
        '#    - Input state to each node\n'
        '#    - Output from each node\n'
        '#    - Time taken per node\n'
        '#    - Total execution time\n'
        '#\n'
        '# 2. LLM CALLS\n'
        '#    - Full prompts sent to LLM\n'
        '#    - Complete responses received\n'
        '#    - Token usage and cost\n'
        '#    - Latency per call\n'
        '#\n'
        '# 3. TOOL CALLS\n'
        '#    - Which tools were invoked\n'
        '#    - Arguments passed\n'
        '#    - Results returned\n'
        '#    - Success/failure status\n'
        '#\n'
        '# 4. STATE CHANGES\n'
        '#    - State before each node\n'
        '#    - State updates from each node\n'
        '#    - Final state output',
        styles
    ))
    
    story.append(tip(
        "LangSmith is free for up to 5,000 traces per month. This is usually enough for "
        "development and testing. Enable it from day one - you will thank yourself when debugging.", styles))
    
    # ========================================================
    # CHAPTER 17: MCP
    # ========================================================
    story.extend(chapter_break(17, "MCP and Agent-to-Agent\nCommunication",
                               "Model Context Protocol and standardized agent interfaces", styles))
    
    story.append(section("17.1 What is MCP?", styles))
    
    story.append(key_term("MCP (Model Context Protocol)",
        "An open protocol by Anthropic that standardizes how AI applications connect to external "
        "data sources and tools. Think of it as a USB standard for AI: any MCP-compatible tool "
        "can plug into any MCP-compatible AI application.", styles))
    
    story.append(body(
        "MCP enables agent-to-agent communication through a standardized interface. Instead of "
        "each agent having custom tool definitions, MCP provides a universal protocol that agents "
        "can use to discover and call tools from other agents or services.", styles))
    
    story.append(code(
        '# 15_mcp_agents.py - MCP Integration Concepts\n'
        '\n'
        '"""\n'
        'MCP Architecture:\n'
        '\n'
        '  +---------+        +------------+        +-----------+\n'
        '  |  Agent  | <----> | MCP Client | <----> | MCP Server|\n'
        '  |  (LLM)  |        | (Protocol) |        | (Tools)   |\n'
        '  +---------+        +------------+        +-----------+\n'
        '\n'
        'The MCP Server exposes:\n'
        '  - Tools (functions the agent can call)\n'
        '  - Resources (data the agent can read)\n'
        '  - Prompts (templates for common tasks)\n'
        '\n'
        'The MCP Client:\n'
        '  - Discovers available tools\n'
        '  - Sends tool call requests\n'
        '  - Receives results\n'
        '  - Maintains session state\n'
        '"""\n'
        '\n'
        '# Example: Agent with MCP-style tool discovery\n'
        'from typing import TypedDict, Annotated\n'
        'from operator import add\n'
        'from langgraph.graph import StateGraph, START, END\n'
        '\n'
        '\n'
        'class MCPState(TypedDict):\n'
        '    query: str\n'
        '    available_tools: list     # Discovered via MCP\n'
        '    tool_results: Annotated[list, add]\n'
        '    response: str\n'
        '\n'
        '\n'
        'def discover_tools(state: MCPState) -> dict:\n'
        '    """Discover available tools from MCP servers.\n'
        '    In production, this queries MCP server endpoints.\n'
        '    """\n'
        '    tools = [\n'
        '        {"name": "database_query", "server": "db-server"},\n'
        '        {"name": "web_search", "server": "search-server"},\n'
        '        {"name": "code_executor", "server": "code-server"},\n'
        '    ]\n'
        '    return {"available_tools": tools}\n'
        '\n'
        '\n'
        'def select_and_call_tools(state: MCPState) -> dict:\n'
        '    """Select appropriate tools and call them via MCP."""\n'
        '    results = []\n'
        '    for tool in state["available_tools"]:\n'
        '        # In production, this sends MCP protocol requests\n'
        '        results.append({\n'
        '            "tool": tool["name"],\n'
        '            "result": f"Result from {tool[\'name\']}"\n'
        '        })\n'
        '    return {"tool_results": results}\n'
        '\n'
        '\n'
        'def synthesize_response(state: MCPState) -> dict:\n'
        '    """Combine tool results into a response."""\n'
        '    context = " | ".join(\n'
        '        r["result"] for r in state["tool_results"]\n'
        '    )\n'
        '    return {"response": f"Based on tools: {context}"}\n'
        '\n'
        '\n'
        'graph = StateGraph(MCPState)\n'
        'graph.add_node("discover", discover_tools)\n'
        'graph.add_node("call_tools", select_and_call_tools)\n'
        'graph.add_node("synthesize", synthesize_response)\n'
        '\n'
        'graph.add_edge(START, "discover")\n'
        'graph.add_edge("discover", "call_tools")\n'
        'graph.add_edge("call_tools", "synthesize")\n'
        'graph.add_edge("synthesize", END)\n'
        '\n'
        'app = graph.compile()\n'
        'result = app.invoke({"query": "Find recent AI papers"})\n'
        'print(result["response"])',
        styles
    ))
    
    # ========================================================
    # CHAPTER 18: Advanced Patterns
    # ========================================================
    story.extend(chapter_break(18, "Advanced Patterns",
                               "Streaming, Map-Reduce, Error Handling, and more", styles))
    
    story.append(section("18.1 Streaming", styles))
    
    story.append(code(
        '# 16_streaming.py\n'
        '\n'
        'from langgraph.graph import StateGraph, MessagesState, START, END\n'
        '\n'
        '\n'
        'def chatbot(state: MessagesState) -> dict:\n'
        '    return {"messages": [{"role": "assistant", "content": "Hello!"}]}\n'
        '\n'
        'graph = StateGraph(MessagesState)\n'
        'graph.add_node("bot", chatbot)\n'
        'graph.add_edge(START, "bot")\n'
        'graph.add_edge("bot", END)\n'
        'app = graph.compile()\n'
        '\n'
        '# Stream node-by-node updates\n'
        'for event in app.stream(\n'
        '    {"messages": [("user", "Hi!")]},\n'
        '    stream_mode="updates"  # or "values" for full state\n'
        '):\n'
        '    print(f"Event: {event}")\n'
        '\n'
        '\n'
        '# Stream modes:\n'
        '# "values"  - Full state after each step\n'
        '# "updates" - Only changes from each node\n'
        '# "debug"   - Detailed execution info',
        styles
    ))
    
    story.append(spacer())
    story.append(section("18.2 Error Handling", styles))
    
    story.append(code(
        '# 18_error_handling.py\n'
        '\n'
        'from typing import TypedDict\n'
        'from langgraph.graph import StateGraph, START, END\n'
        '\n'
        '\n'
        'class ErrorState(TypedDict):\n'
        '    input: str\n'
        '    result: str\n'
        '    error: str\n'
        '    retry_count: int\n'
        '\n'
        '\n'
        'def risky_operation(state: ErrorState) -> dict:\n'
        '    """Operation that might fail."""\n'
        '    try:\n'
        '        # Simulate occasional failure\n'
        '        if state.get("retry_count", 0) < 2:\n'
        '            raise ValueError("Temporary error")\n'
        '        return {"result": "Success!", "error": ""}\n'
        '    except Exception as e:\n'
        '        return {\n'
        '            "error": str(e),\n'
        '            "retry_count": state.get("retry_count", 0) + 1\n'
        '        }\n'
        '\n'
        '\n'
        'def should_retry(state: ErrorState) -> str:\n'
        '    """Decide whether to retry or fail."""\n'
        '    if state.get("error") and state.get("retry_count", 0) < 3:\n'
        '        return "retry"\n'
        '    elif state.get("error"):\n'
        '        return "fail"\n'
        '    return "success"\n'
        '\n'
        '\n'
        'def handle_success(state: ErrorState) -> dict:\n'
        '    return {"result": f"Final: {state[\'result\']}"}\n'
        '\n'
        '\n'
        'def handle_failure(state: ErrorState) -> dict:\n'
        '    return {"result": f"Failed after {state[\'retry_count\']} attempts"}\n'
        '\n'
        '\n'
        'graph = StateGraph(ErrorState)\n'
        'graph.add_node("execute", risky_operation)\n'
        'graph.add_node("success", handle_success)\n'
        'graph.add_node("fail", handle_failure)\n'
        '\n'
        'graph.add_edge(START, "execute")\n'
        'graph.add_conditional_edges("execute", should_retry, {\n'
        '    "retry": "execute",    # Loop back!\n'
        '    "success": "success",\n'
        '    "fail": "fail"\n'
        '})\n'
        'graph.add_edge("success", END)\n'
        'graph.add_edge("fail", END)\n'
        '\n'
        'app = graph.compile()\n'
        'result = app.invoke({"input": "test"})\n'
        'print(result)',
        styles
    ))
    
    # ========================================================
    # CHAPTER 19: Deployment
    # ========================================================
    story.extend(chapter_break(19, "Production Deployment\nand Scaling",
                               "Taking your LangGraph application from prototype to production", styles))
    
    story.append(section("19.1 FastAPI Deployment", styles))
    
    story.append(code(
        '# 19_deployment.py - FastAPI Server\n'
        '\n'
        'from fastapi import FastAPI, HTTPException\n'
        'from pydantic import BaseModel\n'
        'from langgraph.graph import StateGraph, MessagesState, START, END\n'
        'from langgraph.checkpoint.memory import MemorySaver\n'
        '\n'
        '\n'
        'app = FastAPI(title="LangGraph Agent API")\n'
        'checkpointer = MemorySaver()\n'
        '\n'
        '\n'
        'def agent_node(state: MessagesState) -> dict:\n'
        '    last_msg = state["messages"][-1]\n'
        '    content = last_msg.content if hasattr(last_msg, "content") else str(last_msg)\n'
        '    return {"messages": [{"role": "assistant", "content": f"Echo: {content}"}]}\n'
        '\n'
        '\n'
        'graph = StateGraph(MessagesState)\n'
        'graph.add_node("agent", agent_node)\n'
        'graph.add_edge(START, "agent")\n'
        'graph.add_edge("agent", END)\n'
        'agent = graph.compile(checkpointer=checkpointer)\n'
        '\n'
        '\n'
        'class ChatRequest(BaseModel):\n'
        '    message: str\n'
        '    thread_id: str = "default"\n'
        '\n'
        '\n'
        '@app.post("/chat")\n'
        'async def chat(req: ChatRequest):\n'
        '    config = {"configurable": {"thread_id": req.thread_id}}\n'
        '    result = agent.invoke(\n'
        '        {"messages": [("user", req.message)]},\n'
        '        config=config\n'
        '    )\n'
        '    return {"response": result["messages"][-1].content}\n'
        '\n'
        '\n'
        '# Run: uvicorn src.19_deployment:app --reload --port 8000',
        styles
    ))
    
    story.append(spacer())
    story.append(section("19.2 Production Checklist", styles))
    
    story.append(make_table(
        ["Category", "Item", "Priority"],
        [
            ["Persistence", "PostgreSQL checkpointer", "Critical"],
            ["Monitoring", "LangSmith tracing enabled", "Critical"],
            ["Security", "API key rotation", "Critical"],
            ["Performance", "Connection pooling", "High"],
            ["Reliability", "Error handling and retries", "High"],
            ["Scaling", "Async execution", "Medium"],
            ["Testing", "Integration test suite", "High"],
            ["Logging", "Structured logging (JSON)", "Medium"],
        ],
        styles,
    ))
    
    # ========================================================
    # CHAPTER 20: Capstone
    # ========================================================
    story.extend(chapter_break(20, "Capstone Project:\nFull-Stack AI Agent",
                               "Putting everything together in a production-ready system", styles))
    
    story.append(section("20.1 Project Overview", styles))
    
    story.append(body(
        "In this capstone, we build a complete AI assistant that combines everything we have learned. "
        "The system features a supervisor agent that routes to specialist agents, each with their own "
        "tools, memory, and subgraphs. It includes RAG for knowledge retrieval, LangSmith monitoring, "
        "and a FastAPI deployment.", styles))
    
    story.append(code(
        '# 20_capstone_project.py - Full-Stack AI Agent\n'
        '\n'
        '"""\n'
        'ARCHITECTURE:\n'
        '\n'
        '    User Query\n'
        '        |\n'
        '        v\n'
        '    [Supervisor Agent]\n'
        '        |\n'
        '    +---+---+---+\n'
        '    |   |   |   |\n'
        '    v   v   v   v\n'
        '  [RAG] [Code] [Web] [General]\n'
        '  Agent  Agent  Agent  Agent\n'
        '    |     |      |      |\n'
        '    v     v      v      v\n'
        '  Vector  Code   Web    LLM\n'
        '  Store  Exec  Search  Chat\n'
        '    |     |      |      |\n'
        '    +---+-+------+------+\n'
        '        |\n'
        '        v\n'
        '    [Synthesizer]\n'
        '        |\n'
        '        v\n'
        '    Final Response\n'
        '"""\n'
        '\n'
        'from typing import TypedDict, Annotated, Literal\n'
        'from operator import add\n'
        'from langgraph.graph import StateGraph, START, END\n'
        'from langgraph.checkpoint.memory import MemorySaver\n'
        '\n'
        '\n'
        'class AgentState(TypedDict):\n'
        '    query: str\n'
        '    messages: Annotated[list, add]\n'
        '    active_agent: str\n'
        '    context: str\n'
        '    tool_outputs: Annotated[list, add]\n'
        '    final_response: str\n'
        '    iteration: int\n'
        '\n'
        '\n'
        'def supervisor(state: AgentState) -> dict:\n'
        '    """Route to the best specialist agent."""\n'
        '    query = state["query"].lower()\n'
        '    \n'
        '    if any(w in query for w in ["search", "find", "what is"]):\n'
        '        agent = "rag_agent"\n'
        '    elif any(w in query for w in ["code", "python", "function"]):\n'
        '        agent = "code_agent"\n'
        '    elif any(w in query for w in ["news", "latest", "today"]):\n'
        '        agent = "web_agent"\n'
        '    else:\n'
        '        agent = "general_agent"\n'
        '    \n'
        '    return {\n'
        '        "active_agent": agent,\n'
        '        "messages": [f"[Supervisor] Routing to {agent}"]\n'
        '    }\n'
        '\n'
        '\n'
        'def rag_agent(state: AgentState) -> dict:\n'
        '    return {\n'
        '        "context": f"RAG results for: {state[\'query\']}",\n'
        '        "messages": ["[RAG Agent] Retrieved relevant context"],\n'
        '        "tool_outputs": [{"agent": "rag", "data": "knowledge_base_results"}]\n'
        '    }\n'
        '\n'
        '\n'
        'def code_agent(state: AgentState) -> dict:\n'
        '    return {\n'
        '        "context": f"Code solution for: {state[\'query\']}",\n'
        '        "messages": ["[Code Agent] Generated code solution"],\n'
        '        "tool_outputs": [{"agent": "code", "data": "code_output"}]\n'
        '    }\n'
        '\n'
        '\n'
        'def web_agent(state: AgentState) -> dict:\n'
        '    return {\n'
        '        "context": f"Web results for: {state[\'query\']}",\n'
        '        "messages": ["[Web Agent] Searched the web"],\n'
        '        "tool_outputs": [{"agent": "web", "data": "web_results"}]\n'
        '    }\n'
        '\n'
        '\n'
        'def general_agent(state: AgentState) -> dict:\n'
        '    return {\n'
        '        "context": f"General response to: {state[\'query\']}",\n'
        '        "messages": ["[General Agent] Prepared response"],\n'
        '        "tool_outputs": [{"agent": "general", "data": "llm_response"}]\n'
        '    }\n'
        '\n'
        '\n'
        'def synthesizer(state: AgentState) -> dict:\n'
        '    """Combine all agent outputs into final response."""\n'
        '    return {\n'
        '        "final_response": f"Answer: {state[\'context\']}",\n'
        '        "messages": ["[Synthesizer] Final response ready"]\n'
        '    }\n'
        '\n'
        '\n'
        'def route_to_agent(state: AgentState) -> str:\n'
        '    return state["active_agent"]\n'
        '\n'
        '\n'
        '# Build the complete system\n'
        'graph = StateGraph(AgentState)\n'
        '\n'
        'graph.add_node("supervisor", supervisor)\n'
        'graph.add_node("rag_agent", rag_agent)\n'
        'graph.add_node("code_agent", code_agent)\n'
        'graph.add_node("web_agent", web_agent)\n'
        'graph.add_node("general_agent", general_agent)\n'
        'graph.add_node("synthesizer", synthesizer)\n'
        '\n'
        'graph.add_edge(START, "supervisor")\n'
        'graph.add_conditional_edges("supervisor", route_to_agent)\n'
        '\n'
        'for agent in ["rag_agent", "code_agent", "web_agent", "general_agent"]:\n'
        '    graph.add_edge(agent, "synthesizer")\n'
        '\n'
        'graph.add_edge("synthesizer", END)\n'
        '\n'
        'checkpointer = MemorySaver()\n'
        'app = graph.compile(checkpointer=checkpointer)\n'
        '\n'
        '# Test the complete system\n'
        'config = {"configurable": {"thread_id": "capstone_1"}}\n'
        '\n'
        'result = app.invoke(\n'
        '    {"query": "What is retrieval augmented generation?"},\n'
        '    config=config\n'
        ')\n'
        '\n'
        'print("\\n=== EXECUTION TRACE ===")\n'
        'for msg in result["messages"]:\n'
        '    print(f"  {msg}")\n'
        'print(f"\\nFinal: {result[\'final_response\']}")',
        styles
    ))
    
    # ========================================================
    # GLOSSARY
    # ========================================================
    story.append(PageBreak())
    story.append(Spacer(1, 0.5*inch))
    story.append(Paragraph("GLOSSARY OF KEY TERMS", styles['ChapterTitle']))
    story.append(HRFlowable(width="100%", thickness=2, color=COLORS['highlight'], spaceAfter=20))
    
    glossary = [
        ("StateGraph", "The main class for building LangGraph applications. Defines the graph structure."),
        ("TypedDict", "Python typing construct for dictionaries with defined key types. Used for state."),
        ("Pydantic BaseModel", "Data validation library. Alternative to TypedDict with runtime validation."),
        ("Annotated", "Python typing feature that adds metadata to types. Used with reducers."),
        ("Reducer", "A function that defines how state updates merge (e.g., append vs replace)."),
        ("Node", "A function in the graph that processes state and returns updates."),
        ("Edge", "A connection between nodes that defines execution flow."),
        ("Conditional Edge", "An edge that chooses the next node based on state values."),
        ("START / END", "Special constants marking the entry and exit points of a graph."),
        ("Checkpointer", "Persistence layer that saves state at each step for recovery/memory."),
        ("MemorySaver", "In-memory checkpointer for development and testing."),
        ("thread_id", "Unique ID for a conversation thread. Enables multi-user support."),
        ("ToolNode", "Prebuilt node that executes tool calls from LLM responses."),
        ("tools_condition", "Prebuilt router that checks if LLM wants to use tools."),
        ("MessagesState", "Built-in state with an annotated messages list."),
        ("Subgraph", "A compiled graph used as a node within a parent graph."),
        ("MCP", "Model Context Protocol - standardized AI tool communication."),
        ("DAG", "Directed Acyclic Graph - a graph with no cycles (LangGraph supports both)."),
        ("LangSmith", "Monitoring platform for tracing and debugging LangGraph executions."),
        ("bind_tools", "Method to attach tool definitions to an LLM for function calling."),
    ]
    
    for term, defn in glossary:
        story.append(key_term(term, defn, styles))
        story.append(Spacer(1, 2))
    
    # ========================================================
    # BUILD PDF
    # ========================================================
    doc.build(story)
    print(f"Book generated: {output_path}")
    return output_path


if __name__ == "__main__":
    build_book()
